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
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/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 06f944e678..e9484d5b3a 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")
@@ -319,7 +320,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", "€")
@@ -902,12 +903,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
@@ -927,7 +933,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()
@@ -1032,7 +1038,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)
@@ -1104,7 +1110,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)
@@ -1131,7 +1137,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:
@@ -1154,7 +1160,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
@@ -1178,7 +1184,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
@@ -1433,18 +1439,24 @@ 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
- 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
+ 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:
- 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)
@@ -1489,7 +1501,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()
diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py
index 2e83ee9033..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):
@@ -24,16 +26,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 = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = 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,
diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py
index 44f8d2766a..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 type(node) is SceneNode:
+ if not isinstance(node, SceneNode):
continue
if node.callDecoration("getConvexHull"):
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/cura/PrinterOutput/ExtruderOuputModel.py b/cura/PrinterOutput/ExtruderOuputModel.py
new file mode 100644
index 0000000000..b0be6cbbe4
--- /dev/null
+++ b/cura/PrinterOutput/ExtruderOuputModel.py
@@ -0,0 +1,70 @@
+# 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.PrinterOutputModel import PrinterOutputModel
+ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
+
+
+class ExtruderOutputModel(QObject):
+ hotendIDChanged = pyqtSignal()
+ targetHotendTemperatureChanged = pyqtSignal()
+ hotendTemperatureChanged = pyqtSignal()
+ activeMaterialChanged = pyqtSignal()
+
+ 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[MaterialOutputModel]
+
+ @pyqtProperty(QObject, notify = activeMaterialChanged)
+ def activeMaterial(self) -> "MaterialOutputModel":
+ return self._active_material
+
+ def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
+ 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: float):
+ if self._hotend_temperature != temperature:
+ self._hotend_temperature = temperature
+ self.hotendTemperatureChanged.emit()
+
+ 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(float)
+ def setTargetHotendTemperature(self, temperature: float):
+ self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
+ self.updateTargetHotendTemperature(temperature)
+
+ @pyqtProperty(float, notify = targetHotendTemperatureChanged)
+ def targetHotendTemperature(self) -> float:
+ return self._target_hotend_temperature
+
+ @pyqtProperty(float, notify=hotendTemperatureChanged)
+ def hotendTemperature(self) -> float:
+ return self._hotend_temperature
+
+ @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/MaterialOutputModel.py b/cura/PrinterOutput/MaterialOutputModel.py
new file mode 100644
index 0000000000..64ebd3c94c
--- /dev/null
+++ b/cura/PrinterOutput/MaterialOutputModel.py
@@ -0,0 +1,34 @@
+# 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 MaterialOutputModel(QObject):
+ 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):
+ 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
+
+ @pyqtProperty(str, constant=True)
+ def name(self):
+ return self._name
\ No newline at end of file
diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py
new file mode 100644
index 0000000000..5b28ffd30d
--- /dev/null
+++ b/cura/PrinterOutput/NetworkCamera.py
@@ -0,0 +1,119 @@
+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):
+ # 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
+ 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._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._manager = None
+
+ self._started = False
+
+ def getImage(self):
+ return self._image
+
+ ## Ensure that close gets called when object is destroyed
+ def __del__(self):
+ self.stop()
+
+ 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/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py
new file mode 100644
index 0000000000..7cf855ee85
--- /dev/null
+++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py
@@ -0,0 +1,304 @@
+# 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
+
+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, Dict, Tuple
+from enum import IntEnum
+from typing import List
+
+import os # To get the username
+import gzip
+
+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) -> None:
+ super().__init__(device_id = device_id, parent = parent)
+ 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 # 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 = {} # type: Dict[str, Callable[[QNetworkReply], None]]
+ self._authentication_state = AuthState.NotAuthenticated
+
+ # 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._kept_alive_multiparts = {} # type: Dict[QNetworkReply, QHttpMultiPart]
+
+ self._sending_gcode = False
+ self._compressing_gcode = False
+ self._gcode = [] # type: List[str]
+
+ self._connection_state_before_timeout = None # type: Optional[ConnectionState]
+
+ 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) -> None:
+ if self._authentication_state != authentication_state:
+ self._authentication_state = authentication_state
+ self.authenticationStateChanged.emit()
+
+ @pyqtProperty(int, notify=authenticationStateChanged)
+ def authenticationState(self) -> int:
+ return self._authentication_state
+
+ 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.
+
+ # 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) -> Optional[bytes]:
+ self._compressing_gcode = True
+
+ ## Mash the data into single string
+ max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per 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
+
+ # 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.
+ 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 len(batched_lines) != 0:
+ file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines)))
+
+ self._compressing_gcode = False
+ return b"".join(file_data_bytes_list)
+
+ def _update(self) -> bool:
+ 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
+
+ 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:
+ if self._last_manager_create_time is None:
+ 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
+
+ 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:
+ request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
+ request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
+ return request
+
+ def _createFormPart(self, content_header, data, content_type = None) -> QHttpPart:
+ 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) -> 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: QNetworkReply) -> None:
+ 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:
+ self._createNetworkManager()
+ request = self._createEmptyRequest(target)
+ self._last_request_time = time()
+ reply = self._manager.put(request, data.encode())
+ self._registerOnFinishedCallback(reply, onFinished)
+
+ def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None:
+ if self._manager is None:
+ self._createNetworkManager()
+ request = self._createEmptyRequest(target)
+ self._last_request_time = time()
+ reply = self._manager.get(request)
+ 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:
+ self._createNetworkManager()
+ request = self._createEmptyRequest(target)
+ self._last_request_time = time()
+ reply = self._manager.post(request, data)
+ if onProgress is not None:
+ reply.uploadProgress.connect(onProgress)
+ 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:
+ self._createNetworkManager()
+ request = self._createEmptyRequest(target, content_type=None)
+ multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
+ for part in parts:
+ multi_post_part.append(part)
+
+ self._last_request_time = time()
+
+ reply = self._manager.post(request, multi_post_part)
+
+ self._kept_alive_multiparts[reply] = multi_post_part
+
+ if onProgress is not None:
+ reply.uploadProgress.connect(onProgress)
+ 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()
+ post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data)
+ post_part.setBody(body_data)
+
+ self.postFormWithParts(target, [post_part], onFinished, onProgress)
+
+ 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) -> None:
+ Logger.log("d", "Creating network manager")
+ if self._manager:
+ self._manager.finished.disconnect(self.__handleOnFinished)
+ 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)
+
+ 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.
+ 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
+
+ self._last_response_time = time()
+
+ if self._connection_state == ConnectionState.connecting:
+ self.setConnectionState(ConnectionState.connected)
+
+ callback_key = reply.url().toString() + str(reply.operation())
+ try:
+ if callback_key in self._onFinishedCallbacks:
+ self._onFinishedCallbacks[callback_key](reply)
+ except Exception:
+ Logger.logException("w", "something went wrong with callback")
+
+ @pyqtSlot(str, result=str)
+ 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 ""
+
+ 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)
+ def key(self) -> str:
+ return self._id
+
+ ## The IP address of the printer.
+ @pyqtProperty(str, constant=True)
+ 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) -> 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) -> str:
+ return self._properties.get(b"firmware_version", b"").decode("utf-8")
+
+ ## IPadress of this printer
+ @pyqtProperty(str, constant=True)
+ def ipAddress(self) -> str:
+ return self._address
diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py
new file mode 100644
index 0000000000..92376ad1dd
--- /dev/null
+++ b/cura/PrinterOutput/PrintJobOutputModel.py
@@ -0,0 +1,101 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# 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
+ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+
+
+class PrintJobOutputModel(QObject):
+ stateChanged = pyqtSignal()
+ timeTotalChanged = pyqtSignal()
+ timeElapsedChanged = pyqtSignal()
+ nameChanged = pyqtSignal()
+ keyChanged = pyqtSignal()
+ assignedPrinterChanged = pyqtSignal()
+ ownerChanged = pyqtSignal()
+
+ 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 = name # Human readable name
+ self._key = key # Unique identifier
+ 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):
+ return self._assigned_printer
+
+ 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)
+ 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):
+ 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):
+ 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()
+
+ @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
new file mode 100644
index 0000000000..86ca10e2d3
--- /dev/null
+++ b/cura/PrinterOutput/PrinterOutputController.py
@@ -0,0 +1,46 @@
+# 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
+ from cura.PrinterOutput.ExtruderOuputModel import ExtruderOuputModel
+ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+
+
+class PrinterOutputController:
+ 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):
+ Logger.log("w", "Set target hotend temperature not implemented in controller")
+
+ def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
+ Logger.log("w", "Set target bed temperature not implemented in controller")
+
+ def setJobState(self, job: "PrintJobOutputModel", state: str):
+ Logger.log("w", "Set job state not implemented in controller")
+
+ def cancelPreheatBed(self, printer: "PrinterOutputModel"):
+ Logger.log("w", "Cancel preheat bed not implemented in controller")
+
+ def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
+ Logger.log("w", "Preheat bed not implemented in controller")
+
+ def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed):
+ Logger.log("w", "Set head position not implemented in controller")
+
+ def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
+ Logger.log("w", "Move head not implemented in controller")
+
+ def homeBed(self, printer):
+ Logger.log("w", "Home bed not implemented in controller")
+
+ def homeHead(self, printer):
+ Logger.log("w", "Home head not implemented in controller")
\ No newline at end of file
diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py
new file mode 100644
index 0000000000..8234989519
--- /dev/null
+++ b/cura/PrinterOutput/PrinterOutputModel.py
@@ -0,0 +1,240 @@
+# 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
+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.PrinterOutputController import PrinterOutputController
+
+
+class PrinterOutputModel(QObject):
+ bedTemperatureChanged = pyqtSignal()
+ targetBedTemperatureChanged = pyqtSignal()
+ isPreheatingChanged = pyqtSignal()
+ stateChanged = pyqtSignal()
+ activePrintJobChanged = pyqtSignal()
+ nameChanged = pyqtSignal()
+ headPositionChanged = pyqtSignal()
+ keyChanged = pyqtSignal()
+ typeChanged = pyqtSignal()
+ cameraChanged = pyqtSignal()
+
+ 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
+ self._name = ""
+ self._key = "" # Unique identifier
+ self._controller = output_controller
+ 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
+
+ @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
+
+ def updateKey(self, key: str):
+ if self._key != key:
+ self._key = key
+ self.keyChanged.emit()
+
+ @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
+
+ @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.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 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")
+ @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
+
+ @pyqtProperty(str, notify=nameChanged)
+ def name(self):
+ return self._name
+
+ def setName(self, name):
+ self._setName(name)
+ self.updateName(name)
+
+ 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._controller.setTargetBedTemperature(self, temperature)
+ self.updateTargetBedTemperature(temperature)
+
+ def updateActivePrintJob(self, print_job):
+ if self._active_print_job != print_job:
+ 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):
+ if self._printer_state != printer_state:
+ self._printer_state = printer_state
+ self.stateChanged.emit()
+
+ @pyqtProperty(QObject, notify = activePrintJobChanged)
+ def activePrintJob(self):
+ return self._active_print_job
+
+ @pyqtProperty(str, notify=stateChanged)
+ def state(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):
+ 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):
+ if self._controller:
+ 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._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._controller.can_control_manually
+ return False
diff --git a/cura/PrinterOutput/__init__.py b/cura/PrinterOutput/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py
index b147019b37..2aa6fb382e 100644
--- a/cura/PrinterOutputDevice.py
+++ b/cura/PrinterOutputDevice.py
@@ -3,15 +3,21 @@
from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice
-from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal
+from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
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.Application import Application
+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")
## Printer output device adds extra interface options on top of output device.
@@ -25,662 +31,137 @@ 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()
+ 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)
- 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 = [] # type: List[PrinterOutputModel]
self._monitor_view_qml_path = ""
+ self._monitor_component = None
self._monitor_item = None
self._control_view_qml_path = ""
+ self._control_component = None
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._accepts_commands = False
- def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = 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._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)
+
+ def isConnected(self):
+ return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
+
+ def setConnectionState(self, connection_state):
+ if self._connection_state != connection_state:
+ 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
+
+ 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, **kwargs):
raise NotImplementedError("requestWrite needs to be implemented")
- ## Signals
+ @pyqtProperty(QObject, notify = printersChanged)
+ def activePrinter(self) -> Optional["PrinterOutputModel"]:
+ if len(self._printers):
+ return self._printers[0]
+ return None
- # 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
+ @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.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
# create the item (and fail) every time.
- if not self._monitor_item:
+ if not self._monitor_component:
self._createMonitorViewFromQML()
return self._monitor_item
@pyqtProperty(QObject, constant=True)
def controlItem(self):
- if not self._control_item:
+ if not self._control_component:
self._createControlViewFromQML()
+
return self._control_item
def _createControlViewFromQML(self):
if not self._control_view_qml_path:
return
-
- self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {
- "OutputDevice": self
- })
+ if self._control_item is None:
+ self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
def _createMonitorViewFromQML(self):
if not self._monitor_view_qml_path:
return
- self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {
- "OutputDevice": self
- })
-
- @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.findInstanceContainersMetadata(type = "material", GUID = material_id)
- if containers:
- result.append(containers[0]["name"])
- 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.findInstanceContainersMetadata(type = "material", GUID = material_id)
- if containers:
- result.append(containers[0]["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)
+ if self._monitor_item is None:
+ self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
## Attempt to establish connection
def connect(self):
- raise NotImplementedError("connect needs to be implemented")
+ self.setConnectionState(ConnectionState.connecting)
+ 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()
+ self.setConnectionState(ConnectionState.closed)
## 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
+ @pyqtProperty(bool, notify=acceptsCommandsChanged)
+ def acceptsCommands(self):
+ return self._accepts_commands
- ## 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
+ ## 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
- ## 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()
+ self.acceptsCommandsChanged.emit()
## The current processing state of the backend.
@@ -689,4 +170,4 @@ class ConnectionState(IntEnum):
connecting = 1
connected = 2
busy = 3
- error = 4
\ No newline at end of file
+ error = 4
diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py
index 65723db52c..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__()
@@ -41,6 +44,14 @@ 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)
+ 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?
def _calcMaxBuildPlate(self):
@@ -75,11 +86,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
@@ -93,6 +104,7 @@ class CuraSceneController(QObject):
self._build_plate_model.setActiveBuildPlate(nr)
self._objects_model.setActiveBuildPlate(nr)
+ self.activeBuildPlateChanged.emit()
@staticmethod
def createCuraSceneController():
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/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py
index a078240d80..9202e57285 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.
@@ -269,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:
@@ -515,6 +518,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,15 +529,92 @@ 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.qualityChanges.getDefinition().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")
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.
+ 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:
+ 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)
@@ -562,6 +643,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:
diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py
index dbff11279f..b27faa1c61 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,7 +135,7 @@ 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()
@@ -144,8 +144,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)
@@ -170,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/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"):
diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py
index 4491008997..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().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@@ -198,7 +197,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
@@ -206,8 +204,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
@@ -215,8 +213,10 @@ 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] = []
- Logger.log("d", "Build plate %s has 0 objects to be sliced, skipping", 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()
return
self._stored_layer_data = []
@@ -233,10 +233,12 @@ 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()
+ 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
@@ -357,6 +359,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())
@@ -380,7 +394,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
@@ -445,6 +459,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:
@@ -507,7 +522,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()
@@ -547,7 +562,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))
@@ -570,21 +585,23 @@ 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._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.
#
# \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):
@@ -704,7 +721,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)
@@ -715,7 +732,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)
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/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.
diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py
index fa5d6da243..f63ba3ca69 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
@@ -418,11 +418,17 @@ 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.LayerDataDecorator()
+ decorator = LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
@@ -430,7 +436,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..95c48c4d9e 100644
--- a/plugins/GCodeWriter/GCodeWriter.py
+++ b/plugins/GCodeWriter/GCodeWriter.py
@@ -61,8 +61,11 @@ class GCodeWriter(MeshWriter):
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
scene = Application.getInstance().getController().getScene()
- gcode_list = getattr(scene, "gcode_list")[active_build_plate]
- if gcode_list:
+ gcode_dict = getattr(scene, "gcode_dict")
+ if not gcode_dict:
+ return False
+ 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.
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
diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py
index 0736f49858..1a999ca896 100644
--- a/plugins/MonitorStage/MonitorStage.py
+++ b/plugins/MonitorStage/MonitorStage.py
@@ -14,60 +14,122 @@ class MonitorStage(CuraStage):
super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail
- Application.getInstance().engineCreatedSignal.connect(self._setComponents)
+ Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
+ self._printer_output_device = None
- # Update the status icon when the output device is changed
- Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
+ self._active_print_job = None
+ self._active_printer = None
- def _setComponents(self):
- self._setMainOverlay()
- self._setSidebar()
- self._setIconSource()
+ def _setActivePrintJob(self, print_job):
+ if self._active_print_job != print_job:
+ if self._active_print_job:
+ self._active_print_job.stateChanged.disconnect(self._updateIconSource)
+ self._active_print_job = print_job
+ if self._active_print_job:
+ self._active_print_job.stateChanged.connect(self._updateIconSource)
- def _setMainOverlay(self):
+ # Ensure that the right icon source is returned.
+ self._updateIconSource()
+
+ def _setActivePrinter(self, printer):
+ if self._active_printer != printer:
+ if self._active_printer:
+ self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
+ self._active_printer = printer
+ if self._active_printer:
+ self._setActivePrintJob(self._active_printer.activePrintJob)
+ # Jobs might change, so we need to listen to it's changes.
+ self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
+ else:
+ self._setActivePrintJob(None)
+
+ # Ensure that the right icon source is returned.
+ self._updateIconSource()
+
+ def _onActivePrintJobChanged(self):
+ self._setActivePrintJob(self._active_printer.activePrintJob)
+
+ def _onActivePrinterChanged(self):
+ self._setActivePrinter(self._printer_output_device.activePrinter)
+
+ def _onOutputDevicesChanged(self):
+ try:
+ # We assume that you are monitoring the device with the highest priority.
+ new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
+ if new_output_device != self._printer_output_device:
+ if self._printer_output_device:
+ self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
+ self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
+ self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
+
+ self._printer_output_device = new_output_device
+
+ self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource)
+ self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
+ self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
+ self._setActivePrinter(self._printer_output_device.activePrinter)
+
+ # Force an update of the icon source
+ self._updateIconSource()
+ except IndexError:
+ pass
+
+ def _onEngineCreated(self):
+ # We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
+ Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
+ self._onOutputDevicesChanged()
+ self._updateMainOverlay()
+ self._updateSidebar()
+ self._updateIconSource()
+
+ def _updateMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path)
- def _setSidebar(self):
+ def _updateSidebar(self):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)
- def _setIconSource(self):
+ def _updateIconSource(self):
if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self):
- output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
-
- if not output_device:
+ # We assume that you are monitoring the device with the highest priority.
+ try:
+ output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
+ except IndexError:
return "tab_status_unknown"
- if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
+ if not output_device.acceptsCommands:
return "tab_status_unknown"
- if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
- return "tab_status_unknown"
-
- # TODO: refactor to use enum instead of hardcoded strings?
- if output_device.printerState == "maintenance":
- return "tab_status_busy"
-
- if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
- return "tab_status_busy"
-
- if output_device.jobState == "wait_cleanup":
- return "tab_status_finished"
-
- if output_device.jobState in ["ready", ""]:
+ if output_device.activePrinter is None:
return "tab_status_connected"
- if output_device.jobState == "paused":
+ # TODO: refactor to use enum instead of hardcoded strings?
+ if output_device.activePrinter.state == "maintenance":
+ return "tab_status_busy"
+
+ if output_device.activePrinter.activePrintJob is None:
+ return "tab_status_connected"
+
+ if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]:
+ return "tab_status_busy"
+
+ if output_device.activePrinter.activePrintJob.state == "wait_cleanup":
+ return "tab_status_finished"
+
+ if output_device.activePrinter.activePrintJob.state in ["ready", ""]:
+ return "tab_status_connected"
+
+ if output_device.activePrinter.activePrintJob.state == "paused":
return "tab_status_paused"
- if output_device.jobState == "error":
+ if output_device.activePrinter.activePrintJob.state == "error":
return "tab_status_stopped"
return "tab_status_unknown"
diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py
index badca13468..3e1df1c7b8 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
@@ -22,6 +23,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
self._node = None
self._stack = 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:
self._selected_object_id = id
@@ -36,6 +40,10 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
def selectedObjectId(self):
return self._selected_object_id
+ @pyqtSlot(str)
+ def addSkipResetSetting(self, setting_name):
+ self._skip_reset_setting_set.add(setting_name)
+
def setVisible(self, visible):
if not self._node:
return
@@ -50,6 +58,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 instance.definition.key in self._skip_reset_setting_set:
+ 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..eb492d8de2 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
@@ -39,6 +42,13 @@ Item {
verticalAlignment: Text.AlignVCenter
}
+ UM.SettingPropertyProvider
+ {
+ id: meshTypePropertyProvider
+ containerStackId: Cura.MachineManager.activeMachineId
+ watchedProperties: [ "enabled" ]
+ }
+
ComboBox
{
id: meshTypeSelection
@@ -49,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)
@@ -91,6 +120,16 @@ Item {
}
}
+ Connections
+ {
+ target: Cura.MachineManager
+ onGlobalContainerChanged:
+ {
+ meshTypeSelection.model.clear();
+ meshTypeSelection.populateModel();
+ }
+ }
+
Connections
{
target: UM.Selection
@@ -106,7 +145,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 +163,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 +353,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,15 +373,18 @@ 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:
{
// force updating the model to sync it with addedSettingsModel
if(visible)
{
+ // Set skip setting, it will prevent from resetting selected mesh_type
+ contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
listview.model.forceUpdate()
}
}
@@ -394,7 +455,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
{
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 @@
+
+
+
+
\ 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
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
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
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/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/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml
index e78c7d1cc9..df102915ff 100644
--- a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml
+++ b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml
@@ -12,10 +12,10 @@ Component
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
-
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
+
UM.I18nCatalog
{
id: catalog
@@ -33,9 +33,9 @@ Component
horizontalCenter: parent.horizontalCenter
}
- text: OutputDevice.connectedPrinters.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
+ text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
- visible: OutputDevice.connectedPrinters.length == 0
+ visible: OutputDevice.printers.length == 0
}
Item
@@ -46,7 +46,7 @@ Component
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: children.height
- visible: OutputDevice.connectedPrinters.length != 0
+ visible: OutputDevice.printers.length != 0
Label
{
@@ -62,7 +62,6 @@ Component
}
}
-
ScrollView
{
id: printerScrollView
@@ -79,7 +78,7 @@ Component
anchors.fill: parent
spacing: -UM.Theme.getSize("default_lining").height
- model: OutputDevice.connectedPrinters
+ model: OutputDevice.printers
delegate: PrinterInfoBlock
{
@@ -95,7 +94,7 @@ Component
PrinterVideoStream
{
- visible: OutputDevice.selectedPrinterName != ""
+ visible: OutputDevice.activePrinter != null
anchors.fill:parent
}
}
diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
new file mode 100644
index 0000000000..7bdf6090de
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
@@ -0,0 +1,435 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from UM.Logger import Logger
+from UM.Application import Application
+from UM.Settings.ContainerRegistry import ContainerRegistry
+from UM.i18n import i18nCatalog
+from UM.Message import Message
+from UM.Qt.Duration import Duration, DurationFormat
+
+from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
+from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
+from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
+from cura.PrinterOutput.NetworkCamera import NetworkCamera
+
+from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
+
+from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
+from PyQt5.QtGui import QDesktopServices
+from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
+
+from time import time
+from datetime import datetime
+from typing import Optional
+
+import json
+import os
+
+i18n_catalog = i18nCatalog("cura")
+
+
+class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
+ printJobsChanged = pyqtSignal()
+ activePrinterChanged = pyqtSignal()
+
+ # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
+ # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
+ clusterPrintersChanged = pyqtSignal()
+
+ def __init__(self, device_id, address, properties, parent = None):
+ super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
+ self._api_prefix = "/cluster-api/v1/"
+
+ self._number_of_extruders = 2
+
+ self._print_jobs = []
+
+ self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
+ self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
+
+ # See comments about this hack with the clusterPrintersChanged signal
+ self.printersChanged.connect(self.clusterPrintersChanged)
+
+ self._accepts_commands = True
+
+ # Cluster does not have authentication, so default to authenticated
+ self._authentication_state = AuthState.Authenticated
+
+ self._error_message = None
+ self._progress_message = None
+
+ self._active_printer = None # type: Optional[PrinterOutputModel]
+
+ self._printer_selection_dialog = None
+
+ self.setPriority(3) # Make sure the output device gets selected above local file output
+ self.setName(self._id)
+ self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
+ self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
+
+ self._printer_uuid_to_unique_name_mapping = {}
+
+ self._finished_jobs = []
+
+ self._cluster_size = int(properties.get(b"cluster_size", 0))
+
+ def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
+ self.writeStarted.emit(self)
+
+ 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
+
+ if len(self._printers) > 1:
+ self._spawnPrinterSelectionDialog()
+ 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")
+ self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
+ if self._printer_selection_dialog is not None:
+ self._printer_selection_dialog.show()
+
+ @pyqtProperty(int, constant=True)
+ def clusterSize(self):
+ return self._cluster_size
+
+ @pyqtSlot()
+ @pyqtSlot(str)
+ def sendPrintJob(self, target_printer = ""):
+ Logger.log("i", "Sending print job to printer.")
+ if self._sending_gcode:
+ self._error_message = Message(
+ i18n_catalog.i18nc("@info:status",
+ "Sending new jobs (temporarily) blocked, still sending the previous print job."))
+ self._error_message.show()
+ return
+
+ self._sending_gcode = True
+
+ self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
+ i18n_catalog.i18nc("@info:title", "Sending Data"))
+ self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
+ self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
+ self._progress_message.show()
+
+ compressed_gcode = self._compressGCode()
+ if compressed_gcode is None:
+ # Abort was called.
+ return
+
+ parts = []
+
+ # If a specific printer was selected, it should be printed with that machine.
+ if target_printer:
+ target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
+ parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))
+
+ # Add user name to the print_job
+ parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
+
+ file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
+
+ parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode))
+
+ self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
+
+ @pyqtProperty(QObject, notify=activePrinterChanged)
+ def activePrinter(self) -> Optional["PrinterOutputModel"]:
+ return self._active_printer
+
+ @pyqtSlot(QObject)
+ def setActivePrinter(self, printer):
+ if self._active_printer != printer:
+ if self._active_printer and self._active_printer.camera:
+ self._active_printer.camera.stop()
+ self._active_printer = printer
+ self.activePrinterChanged.emit()
+
+ def _onPostPrintJobFinished(self, reply):
+ self._progress_message.hide()
+ self._compressing_gcode = False
+ self._sending_gcode = False
+
+ def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
+ if bytes_total > 0:
+ new_progress = bytes_sent / bytes_total * 100
+ # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
+ # timeout responses if this happens.
+ self._last_response_time = time()
+ if new_progress > self._progress_message.getProgress():
+ self._progress_message.show() # Ensure that the message is visible.
+ self._progress_message.setProgress(bytes_sent / bytes_total * 100)
+ else:
+ self._progress_message.setProgress(0)
+ self._progress_message.hide()
+
+ def _progressMessageActionTriggered(self, message_id=None, action_id=None):
+ if action_id == "Abort":
+ Logger.log("d", "User aborted sending print to remote.")
+ self._progress_message.hide()
+ self._compressing_gcode = False
+ self._sending_gcode = False
+ Application.getInstance().getController().setActiveStage("PrepareStage")
+
+ @pyqtSlot()
+ def openPrintJobControlPanel(self):
+ Logger.log("d", "Opening print job control panel...")
+ QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
+
+ @pyqtSlot()
+ def openPrinterControlPanel(self):
+ Logger.log("d", "Opening printer control panel...")
+ QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
+
+ @pyqtProperty("QVariantList", notify=printJobsChanged)
+ def printJobs(self):
+ return self._print_jobs
+
+ @pyqtProperty("QVariantList", notify=printJobsChanged)
+ def queuedPrintJobs(self):
+ return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None]
+
+ @pyqtProperty("QVariantList", notify=printJobsChanged)
+ def activePrintJobs(self):
+ return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None]
+
+ @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
+ def connectedPrintersTypeCount(self):
+ printer_count = {}
+ for printer in self._printers:
+ if printer.type in printer_count:
+ printer_count[printer.type] += 1
+ else:
+ printer_count[printer.type] = 1
+ result = []
+ for machine_type in printer_count:
+ result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
+ return result
+
+ @pyqtSlot(int, result=str)
+ def formatDuration(self, seconds):
+ return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
+
+ @pyqtSlot(int, result=str)
+ def getTimeCompleted(self, time_remaining):
+ current_time = time()
+ datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
+ return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
+
+ @pyqtSlot(int, result=str)
+ def getDateCompleted(self, time_remaining):
+ current_time = time()
+ datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
+ return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
+
+ def _printJobStateChanged(self):
+ username = self._getUserName()
+
+ if username is None:
+ return # We only want to show notifications if username is set.
+
+ finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]
+
+ newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
+ for job in newly_finished_jobs:
+ job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
+ job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
+ job_completed_message.show()
+
+ # Ensure UI gets updated
+ self.printJobsChanged.emit()
+
+ # Keep a list of all completed jobs so we know if something changed next time.
+ self._finished_jobs = finished_jobs
+
+ def _update(self):
+ if not super()._update():
+ return
+ self.get("printers/", onFinished=self._onGetPrintersDataFinished)
+ self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
+
+ def _onGetPrintJobsFinished(self, reply: QNetworkReply):
+ if not checkValidGetReply(reply):
+ return
+
+ result = loadJsonFromReply(reply)
+ if result is None:
+ return
+
+ print_jobs_seen = []
+ job_list_changed = False
+ for print_job_data in result:
+ print_job = findByKey(self._print_jobs, print_job_data["uuid"])
+
+ if print_job is None:
+ print_job = self._createPrintJobModel(print_job_data)
+ job_list_changed = True
+
+ self._updatePrintJob(print_job, print_job_data)
+
+ if print_job.state != "queued": # Print job should be assigned to a printer.
+ printer = self._getPrinterByKey(print_job_data["printer_uuid"])
+ else: # The job can "reserve" a printer if some changes are required.
+ printer = self._getPrinterByKey(print_job_data["assigned_to"])
+
+ if printer:
+ printer.updateActivePrintJob(print_job)
+
+ print_jobs_seen.append(print_job)
+
+ # Check what jobs need to be removed.
+ removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]
+
+ for removed_job in removed_jobs:
+ job_list_changed |= self._removeJob(removed_job)
+
+ if job_list_changed:
+ self.printJobsChanged.emit() # Do a single emit for all print job changes.
+
+ def _onGetPrintersDataFinished(self, reply: QNetworkReply):
+ if not checkValidGetReply(reply):
+ return
+
+ result = loadJsonFromReply(reply)
+ if result is None:
+ return
+
+ printer_list_changed = False
+ printers_seen = []
+
+ for printer_data in result:
+ printer = findByKey(self._printers, printer_data["uuid"])
+
+ if printer is None:
+ printer = self._createPrinterModel(printer_data)
+ printer_list_changed = True
+
+ printers_seen.append(printer)
+
+ self._updatePrinter(printer, printer_data)
+
+ removed_printers = [printer for printer in self._printers if printer not in printers_seen]
+ for printer in removed_printers:
+ self._removePrinter(printer)
+
+ if removed_printers or printer_list_changed:
+ self.printersChanged.emit()
+
+ def _createPrinterModel(self, data):
+ printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
+ number_of_extruders=self._number_of_extruders)
+ printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
+ self._printers.append(printer)
+ return printer
+
+ def _createPrintJobModel(self, data):
+ print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
+ key=data["uuid"], name= data["name"])
+ print_job.stateChanged.connect(self._printJobStateChanged)
+ self._print_jobs.append(print_job)
+ return print_job
+
+ def _updatePrintJob(self, print_job, data):
+ print_job.updateTimeTotal(data["time_total"])
+ print_job.updateTimeElapsed(data["time_elapsed"])
+ print_job.updateState(data["status"])
+ print_job.updateOwner(data["owner"])
+
+ def _updatePrinter(self, printer, data):
+ # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
+ # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
+ self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
+
+ printer.updateName(data["friendly_name"])
+ printer.updateKey(data["uuid"])
+ printer.updateType(data["machine_variant"])
+ if not data["enabled"]:
+ printer.updateState("disabled")
+ else:
+ printer.updateState(data["status"])
+
+ for index in range(0, self._number_of_extruders):
+ extruder = printer.extruders[index]
+ try:
+ extruder_data = data["configuration"][index]
+ except IndexError:
+ break
+
+ extruder.updateHotendID(extruder_data.get("print_core_id", ""))
+
+ material_data = extruder_data["material"]
+ if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
+ containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
+ GUID=material_data["guid"])
+ if containers:
+ color = containers[0].getMetaDataEntry("color_code")
+ brand = containers[0].getMetaDataEntry("brand")
+ material_type = containers[0].getMetaDataEntry("material")
+ name = containers[0].getName()
+ else:
+ Logger.log("w",
+ "Unable to find material with guid {guid}. Using data as provided by cluster".format(
+ guid=material_data["guid"]))
+ color = material_data["color"]
+ brand = material_data["brand"]
+ material_type = material_data["material"]
+ name = "Unknown"
+
+ material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
+ brand=brand, color=color, name=name)
+ extruder.updateActiveMaterial(material)
+
+ def _removeJob(self, job):
+ if job not in self._print_jobs:
+ return False
+
+ if job.assignedPrinter:
+ job.assignedPrinter.updateActivePrintJob(None)
+ job.stateChanged.disconnect(self._printJobStateChanged)
+ self._print_jobs.remove(job)
+
+ return True
+
+ def _removePrinter(self, printer):
+ self._printers.remove(printer)
+ if self._active_printer == printer:
+ self._active_printer = None
+ self.activePrinterChanged.emit()
+
+
+def loadJsonFromReply(reply):
+ try:
+ result = json.loads(bytes(reply.readAll()).decode("utf-8"))
+ except json.decoder.JSONDecodeError:
+ Logger.logException("w", "Unable to decode JSON from reply.")
+ return
+ return result
+
+
+def checkValidGetReply(reply):
+ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
+
+ if status_code != 200:
+ Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code))
+ return False
+ return True
+
+
+def findByKey(list, key):
+ for item in list:
+ if item.key == key:
+ return item
\ No newline at end of file
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)
+
diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py
index 0d5d259ce4..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"
@@ -25,24 +28,24 @@ class DiscoverUM3Action(MachineAction):
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
- self._last_zeroconf_event_time = time.time()
- self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset
+ self._last_zero_conf_event_time = time.time()
- printersChanged = pyqtSignal()
+ # Time to wait after a zero-conf service change before allowing a zeroconf reset
+ self._zero_conf_change_grace_period = 0.25
@pyqtSlot()
def startDiscovery(self):
if not self._network_plugin:
- Logger.log("d", "Starting printer discovery.")
+ Logger.log("d", "Starting device discovery.")
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
- self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
- self.printersChanged.emit()
+ self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
+ self.discoveredDevicesChanged.emit()
- ## Re-filters the list of printers.
+ ## Re-filters the list of devices.
@pyqtSlot()
def reset(self):
- Logger.log("d", "Reset the list of found printers.")
- self.printersChanged.emit()
+ Logger.log("d", "Reset the list of found devices.")
+ self.discoveredDevicesChanged.emit()
@pyqtSlot()
def restartDiscovery(self):
@@ -51,43 +54,44 @@ class DiscoverUM3Action(MachineAction):
# It's most likely that the QML engine is still creating delegates, where the python side already deleted or
# garbage collected the data.
# Whatever the case, waiting a bit ensures that it doesn't crash.
- if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period:
+ if time.time() - self._last_zero_conf_event_time > self._zero_conf_change_grace_period:
if not self._network_plugin:
self.startDiscovery()
else:
self._network_plugin.startDiscovery()
@pyqtSlot(str, str)
- def removeManualPrinter(self, key, address):
+ def removeManualDevice(self, key, address):
if not self._network_plugin:
return
- self._network_plugin.removeManualPrinter(key, address)
+ self._network_plugin.removeManualDevice(key, address)
@pyqtSlot(str, str)
- def setManualPrinter(self, key, address):
+ def setManualDevice(self, key, address):
if key != "":
# This manual printer replaces a current manual printer
- self._network_plugin.removeManualPrinter(key)
+ self._network_plugin.removeManualDevice(key)
if address != "":
- self._network_plugin.addManualPrinter(address)
+ self._network_plugin.addManualDevice(address)
- def _onPrinterDiscoveryChanged(self, *args):
- self._last_zeroconf_event_time = time.time()
- self.printersChanged.emit()
+ def _onDeviceDiscoveryChanged(self, *args):
+ self._last_zero_conf_event_time = time.time()
+ self.discoveredDevicesChanged.emit()
- @pyqtProperty("QVariantList", notify = printersChanged)
+ @pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
def foundDevices(self):
if self._network_plugin:
+ # TODO: Check if this needs to stay.
if Application.getInstance().getGlobalContainerStack():
global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId()
else:
global_printer_type = "unknown"
- printers = list(self._network_plugin.getPrinters().values())
+ printers = list(self._network_plugin.getDiscoveredDevices().values())
# TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet.
- printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
+ #printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
printers.sort(key = lambda k: k.name)
return printers
else:
diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml
index cb574384f9..0e58d8e991 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
@@ -29,9 +29,9 @@ Cura.MachineAction
function connectToPrinter()
{
- if(base.selectedPrinter && base.completeProperties)
+ if(base.selectedDevice && base.completeProperties)
{
- var printerKey = base.selectedPrinter.getKey()
+ var printerKey = base.selectedDevice.key
if(manager.getStoredKey() != printerKey)
{
manager.setKey(printerKey);
@@ -83,10 +83,10 @@ Cura.MachineAction
{
id: editButton
text: catalog.i18nc("@action:button", "Edit")
- enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
+ enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
onClicked:
{
- manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress);
+ manualPrinterDialog.showDialog(base.selectedDevice.key, base.selectedDevice.ipAddress);
}
}
@@ -94,8 +94,8 @@ Cura.MachineAction
{
id: removeButton
text: catalog.i18nc("@action:button", "Remove")
- enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
- onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress)
+ enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
+ onClicked: manager.removeManualDevice(base.selectedDevice.key, base.selectedDevice.ipAddress)
}
Button
@@ -139,7 +139,7 @@ Cura.MachineAction
{
var selectedKey = manager.getStoredKey();
for(var i = 0; i < model.length; i++) {
- if(model[i].getKey() == selectedKey)
+ if(model[i].key == selectedKey)
{
currentIndex = i;
return
@@ -151,9 +151,9 @@ Cura.MachineAction
currentIndex: -1
onCurrentIndexChanged:
{
- base.selectedPrinter = listview.model[currentIndex];
+ base.selectedDevice = listview.model[currentIndex];
// Only allow connecting if the printer has responded to API query since the last refresh
- base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true";
+ base.completeProperties = base.selectedDevice != null && base.selectedDevice.getProperty("incomplete") != "true";
}
Component.onCompleted: manager.startDiscovery()
delegate: Rectangle
@@ -199,13 +199,13 @@ Cura.MachineAction
Column
{
width: Math.floor(parent.width * 0.5)
- visible: base.selectedPrinter ? true : false
+ visible: base.selectedDevice ? true : false
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width
wrapMode: Text.WordWrap
- text: base.selectedPrinter ? base.selectedPrinter.name : ""
+ text: base.selectedDevice ? base.selectedDevice.name : ""
font: UM.Theme.getFont("large")
elide: Text.ElideRight
}
@@ -226,17 +226,17 @@ Cura.MachineAction
wrapMode: Text.WordWrap
text:
{
- if(base.selectedPrinter)
+ if(base.selectedDevice)
{
- if(base.selectedPrinter.printerType == "ultimaker3")
+ if(base.selectedDevice.printerType == "ultimaker3")
{
- return catalog.i18nc("@label Printer name", "Ultimaker 3")
- } else if(base.selectedPrinter.printerType == "ultimaker3_extended")
+ return catalog.i18nc("@label", "Ultimaker 3")
+ } else if(base.selectedDevice.printerType == "ultimaker3_extended")
{
- return catalog.i18nc("@label Printer name", "Ultimaker 3 Extended")
+ return catalog.i18nc("@label", "Ultimaker 3 Extended")
} else
{
- return catalog.i18nc("@label Printer name", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
+ return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
}
}
else
@@ -255,7 +255,7 @@ Cura.MachineAction
{
width: Math.floor(parent.width * 0.5)
wrapMode: Text.WordWrap
- text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : ""
+ text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
}
Label
{
@@ -267,7 +267,7 @@ Cura.MachineAction
{
width: Math.floor(parent.width * 0.5)
wrapMode: Text.WordWrap
- text: base.selectedPrinter ? base.selectedPrinter.ipAddress : ""
+ text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
}
}
@@ -277,17 +277,17 @@ Cura.MachineAction
wrapMode: Text.WordWrap
text:{
// The property cluster size does not exist for older UM3 devices.
- if(!base.selectedPrinter || base.selectedPrinter.clusterSize == null || base.selectedPrinter.clusterSize == 1)
+ if(!base.selectedDevice || base.selectedDevice.clusterSize == null || base.selectedDevice.clusterSize == 1)
{
return "";
}
- else if (base.selectedPrinter.clusterSize === 0)
+ else if (base.selectedDevice.clusterSize === 0)
{
return catalog.i18nc("@label", "This printer is not set up to host a group of Ultimaker 3 printers.");
}
else
{
- return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedPrinter.clusterSize));
+ return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedDevice.clusterSize));
}
}
@@ -296,14 +296,14 @@ Cura.MachineAction
{
width: parent.width
wrapMode: Text.WordWrap
- visible: base.selectedPrinter != null && !base.completeProperties
+ visible: base.selectedDevice != null && !base.completeProperties
text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
}
Button
{
text: catalog.i18nc("@action:button", "Connect")
- enabled: (base.selectedPrinter && base.completeProperties) ? true : false
+ enabled: (base.selectedDevice && base.completeProperties) ? true : false
onClicked: connectToPrinter()
}
}
@@ -337,7 +337,7 @@ Cura.MachineAction
onAccepted:
{
- manager.setManualPrinter(printerKey, addressText)
+ manager.setManualDevice(printerKey, addressText)
}
Column {
diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
new file mode 100644
index 0000000000..a63adadd54
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
@@ -0,0 +1,632 @@
+from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
+from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
+from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
+from cura.PrinterOutput.NetworkCamera import NetworkCamera
+
+from cura.Settings.ContainerManager import ContainerManager
+from cura.Settings.ExtruderManager import ExtruderManager
+
+from UM.Logger import Logger
+from UM.Settings.ContainerRegistry import ContainerRegistry
+from UM.Application import Application
+from UM.i18n import i18nCatalog
+from UM.Message import Message
+
+from PyQt5.QtNetwork import QNetworkRequest
+from PyQt5.QtCore import QTimer, QCoreApplication
+from PyQt5.QtWidgets import QMessageBox
+
+from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController
+
+from time import time
+
+import json
+import os
+
+
+i18n_catalog = i18nCatalog("cura")
+
+
+## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API.
+# Everything after that firmware uses the ClusterUM3Output.
+# The Legacy output device can only have one printer (whereas the cluster can have 0 to n).
+#
+# Authentication is done in a number of steps;
+# 1. Request an id / key pair by sending the application & user name. (state = authRequested)
+# 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived)
+# 3. OutputDevice will poll if the button was pressed.
+# 4. At this point the machine either has the state Authenticated or AuthenticationDenied.
+# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
+class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
+ def __init__(self, device_id, address: str, properties, parent = None):
+ super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
+ self._api_prefix = "/api/v1/"
+ self._number_of_extruders = 2
+
+ self._authentication_id = None
+ self._authentication_key = None
+
+ self._authentication_counter = 0
+ self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min)
+
+ self._authentication_timer = QTimer()
+ self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval
+ self._authentication_timer.setSingleShot(False)
+
+ self._authentication_timer.timeout.connect(self._onAuthenticationTimer)
+
+ # The messages are created when connect is called the first time.
+ # This ensures that the messages are only created for devices that actually want to connect.
+ self._authentication_requested_message = None
+ self._authentication_failed_message = None
+ self._authentication_succeeded_message = None
+ self._not_authenticated_message = None
+
+ self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)
+
+ self.setPriority(3) # Make sure the output device gets selected above local file output
+ self.setName(self._id)
+ self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
+ self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
+
+ self.setIconName("print")
+
+ self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
+
+ self._output_controller = LegacyUM3PrinterOutputController(self)
+
+ def _onAuthenticationStateChanged(self):
+ # We only accept commands if we are authenticated.
+ if self._authentication_state == AuthState.Authenticated:
+ self._setAcceptsCommands(True)
+ else:
+ self._setAcceptsCommands(False)
+
+ def _setupMessages(self):
+ self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
+ "Access to the printer requested. Please approve the request on the printer"),
+ lifetime=0, dismissable=False, progress=0,
+ title=i18n_catalog.i18nc("@info:title",
+ "Authentication status"))
+
+ self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""),
+ title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
+ self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
+ i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
+ self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
+ self._authentication_succeeded_message = Message(
+ i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
+ title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
+
+ self._not_authenticated_message = Message(
+ i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
+ title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
+ self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
+ None, i18n_catalog.i18nc("@info:tooltip",
+ "Send access request to the printer"))
+ self._not_authenticated_message.actionTriggered.connect(self._messageCallback)
+
+ def _messageCallback(self, message_id=None, action_id="Retry"):
+ if action_id == "Request" or action_id == "Retry":
+ if self._authentication_failed_message:
+ self._authentication_failed_message.hide()
+ if self._not_authenticated_message:
+ self._not_authenticated_message.hide()
+
+ self._requestAuthentication()
+
+ def connect(self):
+ super().connect()
+ self._setupMessages()
+ global_container = Application.getInstance().getGlobalContainerStack()
+ if global_container:
+ self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
+ self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)
+
+ def close(self):
+ super().close()
+ if self._authentication_requested_message:
+ self._authentication_requested_message.hide()
+ if self._authentication_failed_message:
+ self._authentication_failed_message.hide()
+ if self._authentication_succeeded_message:
+ self._authentication_succeeded_message.hide()
+ self._sending_gcode = False
+ self._compressing_gcode = False
+ self._authentication_timer.stop()
+
+ ## Send all material profiles to the printer.
+ def _sendMaterialProfiles(self):
+ Logger.log("i", "Sending material profiles to printer")
+
+ # TODO: Might want to move this to a job...
+ for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
+ try:
+ xml_data = container.serialize()
+ if xml_data == "" or xml_data is None:
+ continue
+
+ names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
+ if names:
+ # There are other materials that share this GUID.
+ if not container.isReadOnly():
+ continue # If it's not readonly, it's created by user, so skip it.
+
+ file_name = "none.xml"
+
+ self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None)
+
+ except NotImplementedError:
+ # If the material container is not the most "generic" one it can't be serialized an will raise a
+ # NotImplementedError. We can simply ignore these.
+ pass
+
+ def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
+ if not self.activePrinter:
+ # No active printer. Unable to write
+ return
+
+ if self.activePrinter.state not in ["idle", ""]:
+ # Printer is not able to accept commands.
+ return
+
+ if self._authentication_state != AuthState.Authenticated:
+ # Not authenticated, so unable to send job.
+ return
+
+ self.writeStarted.emit(self)
+
+ 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.")
+ 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()
+
+ # 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:
+ self._error_message = Message(
+ i18n_catalog.i18nc("@info:status",
+ "Sending new jobs (temporarily) blocked, still sending the previous print job."))
+ self._error_message.show()
+ return
+
+ self._sending_gcode = True
+
+ self._send_gcode_start = time()
+ self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
+ i18n_catalog.i18nc("@info:title", "Sending Data"))
+ self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
+ self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
+ self._progress_message.show()
+
+ compressed_gcode = self._compressGCode()
+ if compressed_gcode is None:
+ # Abort was called.
+ return
+
+ file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
+ self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
+ onFinished=self._onPostPrintJobFinished)
+
+ return
+
+ def _progressMessageActionTriggered(self, message_id=None, action_id=None):
+ if action_id == "Abort":
+ Logger.log("d", "User aborted sending print to remote.")
+ self._progress_message.hide()
+ self._compressing_gcode = False
+ self._sending_gcode = False
+ Application.getInstance().getController().setActiveStage("PrepareStage")
+
+ def _onPostPrintJobFinished(self, reply):
+ self._progress_message.hide()
+ self._sending_gcode = False
+
+ def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
+ if bytes_total > 0:
+ new_progress = bytes_sent / bytes_total * 100
+ # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
+ # timeout responses if this happens.
+ self._last_response_time = time()
+ if new_progress > self._progress_message.getProgress():
+ self._progress_message.show() # Ensure that the message is visible.
+ self._progress_message.setProgress(bytes_sent / bytes_total * 100)
+ else:
+ self._progress_message.setProgress(0)
+
+ self._progress_message.hide()
+
+ def _messageBoxCallback(self, button):
+ def delayedCallback():
+ if button == QMessageBox.Yes:
+ self._startPrint()
+ else:
+ Application.getInstance().getController().setActiveStage("PrepareStage")
+ # For some unknown reason Cura on OSX will hang if we do the call back code
+ # immediately without first returning and leaving QML's event system.
+
+ QTimer.singleShot(100, delayedCallback)
+
+ def _checkForErrors(self):
+ errors = []
+ print_information = Application.getInstance().getPrintInformation()
+ if not print_information.materialLengths:
+ Logger.log("w", "There is no material length information. Unable to check for errors.")
+ return errors
+
+ for index, extruder in enumerate(self.activePrinter.extruders):
+ # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
+ if extruder.hotendID == "":
+ # No Printcore loaded.
+ errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))
+
+ if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
+ # The extruder is by this print.
+ if extruder.activeMaterial is None:
+ # No active material
+ errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
+ return errors
+
+ def _checkForWarnings(self):
+ warnings = []
+ print_information = Application.getInstance().getPrintInformation()
+
+ if not print_information.materialLengths:
+ Logger.log("w", "There is no material length information. Unable to check for warnings.")
+ return warnings
+
+ extruder_manager = ExtruderManager.getInstance()
+
+ for index, extruder in enumerate(self.activePrinter.extruders):
+ if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
+ # The extruder is by this print.
+
+ # TODO: material length check
+
+ # Check if the right Printcore is active.
+ variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
+ if variant:
+ if variant.getName() != extruder.hotendID:
+ warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
+ else:
+ Logger.log("w", "Unable to find variant.")
+
+ # Check if the right material is loaded.
+ local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
+ if local_material:
+ if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
+ Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
+ warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
+ else:
+ Logger.log("w", "Unable to find material.")
+
+ return warnings
+
+ def _update(self):
+ if not super()._update():
+ return
+ if self._authentication_state == AuthState.NotAuthenticated:
+ if self._authentication_id is None and self._authentication_key is None:
+ # This machine doesn't have any authentication, so request it.
+ self._requestAuthentication()
+ elif self._authentication_id is not None and self._authentication_key is not None:
+ # We have authentication info, but we haven't checked it out yet. Do so now.
+ self._verifyAuthentication()
+ elif self._authentication_state == AuthState.AuthenticationReceived:
+ # We have an authentication, but it's not confirmed yet.
+ self._checkAuthentication()
+
+ # We don't need authentication for requesting info, so we can go right ahead with requesting this.
+ self.get("printer", onFinished=self._onGetPrinterDataFinished)
+ self.get("print_job", onFinished=self._onGetPrintJobFinished)
+
+ def _resetAuthenticationRequestedMessage(self):
+ if self._authentication_requested_message:
+ self._authentication_requested_message.hide()
+ self._authentication_timer.stop()
+ self._authentication_counter = 0
+
+ def _onAuthenticationTimer(self):
+ self._authentication_counter += 1
+ self._authentication_requested_message.setProgress(
+ self._authentication_counter / self._max_authentication_counter * 100)
+ if self._authentication_counter > self._max_authentication_counter:
+ self._authentication_timer.stop()
+ Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
+ self.setAuthenticationState(AuthState.AuthenticationDenied)
+ self._resetAuthenticationRequestedMessage()
+ self._authentication_failed_message.show()
+
+ def _verifyAuthentication(self):
+ Logger.log("d", "Attempting to verify authentication")
+ # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
+ self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted)
+
+ def _onVerifyAuthenticationCompleted(self, reply):
+ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
+ if status_code == 401:
+ # Something went wrong; We somehow tried to verify authentication without having one.
+ Logger.log("d", "Attempted to verify auth without having one.")
+ self._authentication_id = None
+ self._authentication_key = None
+ self.setAuthenticationState(AuthState.NotAuthenticated)
+ elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
+ # If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
+ Logger.log("d",
+ "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
+ self._authentication_state)
+ self.setAuthenticationState(AuthState.AuthenticationDenied)
+ self._authentication_failed_message.show()
+ elif status_code == 200:
+ self.setAuthenticationState(AuthState.Authenticated)
+ # Now we know for sure that we are authenticated, send the material profiles to the machine.
+ self._sendMaterialProfiles()
+
+ def _checkAuthentication(self):
+ Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
+ self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished)
+
+ def _onCheckAuthenticationFinished(self, reply):
+ if str(self._authentication_id) not in reply.url().toString():
+ Logger.log("w", "Got an old id response.")
+ # Got response for old authentication ID.
+ return
+ try:
+ data = json.loads(bytes(reply.readAll()).decode("utf-8"))
+ except json.decoder.JSONDecodeError:
+ Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
+ return
+
+ if data.get("message", "") == "authorized":
+ Logger.log("i", "Authentication was approved")
+ self.setAuthenticationState(AuthState.Authenticated)
+ self._saveAuthentication()
+
+ # Double check that everything went well.
+ self._verifyAuthentication()
+
+ # Notify the user.
+ self._resetAuthenticationRequestedMessage()
+ self._authentication_succeeded_message.show()
+ elif data.get("message", "") == "unauthorized":
+ Logger.log("i", "Authentication was denied.")
+ self.setAuthenticationState(AuthState.AuthenticationDenied)
+ self._authentication_failed_message.show()
+
+ def _saveAuthentication(self):
+ global_container_stack = Application.getInstance().getGlobalContainerStack()
+ if global_container_stack:
+ if "network_authentication_key" in global_container_stack.getMetaData():
+ global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)
+ else:
+ global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key)
+
+ if "network_authentication_id" in global_container_stack.getMetaData():
+ global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)
+ else:
+ global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
+
+ # Force save so we are sure the data is not lost.
+ Application.getInstance().saveStack(global_container_stack)
+ Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
+ self._getSafeAuthKey())
+ else:
+ Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
+ self._getSafeAuthKey())
+
+ def _onRequestAuthenticationFinished(self, reply):
+ try:
+ data = json.loads(bytes(reply.readAll()).decode("utf-8"))
+ except json.decoder.JSONDecodeError:
+ Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
+ self.setAuthenticationState(AuthState.NotAuthenticated)
+ return
+
+ self.setAuthenticationState(AuthState.AuthenticationReceived)
+ self._authentication_id = data["id"]
+ self._authentication_key = data["key"]
+ Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
+ self._authentication_id, self._getSafeAuthKey())
+
+ def _requestAuthentication(self):
+ self._authentication_requested_message.show()
+ self._authentication_timer.start()
+
+ # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
+ # give issues.
+ self._authentication_key = None
+ self._authentication_id = None
+
+ self.post("auth/request",
+ json.dumps({"application": "Cura-" + Application.getInstance().getVersion(),
+ "user": self._getUserName()}).encode(),
+ onFinished=self._onRequestAuthenticationFinished)
+
+ self.setAuthenticationState(AuthState.AuthenticationRequested)
+
+ def _onAuthenticationRequired(self, reply, authenticator):
+ if self._authentication_id is not None and self._authentication_key is not None:
+ Logger.log("d",
+ "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
+ self._id, self._authentication_id, self._getSafeAuthKey())
+ authenticator.setUser(self._authentication_id)
+ authenticator.setPassword(self._authentication_key)
+ else:
+ Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)
+
+ def _onGetPrintJobFinished(self, reply):
+ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
+
+ if not self._printers:
+ return # Ignore the data for now, we don't have info about a printer yet.
+ printer = self._printers[0]
+
+ if status_code == 200:
+ try:
+ result = json.loads(bytes(reply.readAll()).decode("utf-8"))
+ except json.decoder.JSONDecodeError:
+ Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
+ return
+ if printer.activePrintJob is None:
+ print_job = PrintJobOutputModel(output_controller=self._output_controller)
+ printer.updateActivePrintJob(print_job)
+ else:
+ print_job = printer.activePrintJob
+ print_job.updateState(result["state"])
+ print_job.updateTimeElapsed(result["time_elapsed"])
+ print_job.updateTimeTotal(result["time_total"])
+ print_job.updateName(result["name"])
+ elif status_code == 404:
+ # No job found, so delete the active print job (if any!)
+ printer.updateActivePrintJob(None)
+ else:
+ Logger.log("w",
+ "Got status code {status_code} while trying to get printer data".format(status_code=status_code))
+
+ def materialHotendChangedMessage(self, callback):
+ Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
+ i18n_catalog.i18nc("@label",
+ "Would you like to use your current printer configuration in Cura?"),
+ i18n_catalog.i18nc("@label",
+ "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
+ buttons=QMessageBox.Yes + QMessageBox.No,
+ icon=QMessageBox.Question,
+ callback=callback
+ )
+
+ def _onGetPrinterDataFinished(self, reply):
+ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
+ if status_code == 200:
+ try:
+ result = json.loads(bytes(reply.readAll()).decode("utf-8"))
+ except json.decoder.JSONDecodeError:
+ Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
+ return
+
+ if not self._printers:
+ # Quickest way to get the firmware version is to grab it from the zeroconf.
+ firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
+ self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
+ self._printers[0].setCamera(NetworkCamera("http://" + self._address + ":8080/?action=stream"))
+ for extruder in self._printers[0].extruders:
+ extruder.activeMaterialChanged.connect(self.materialIdChanged)
+ extruder.hotendIDChanged.connect(self.hotendIdChanged)
+ self.printersChanged.emit()
+
+ # LegacyUM3 always has a single printer.
+ printer = self._printers[0]
+ printer.updateBedTemperature(result["bed"]["temperature"]["current"])
+ printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
+ printer.updateState(result["status"])
+
+ try:
+ # If we're still handling the request, we should ignore remote for a bit.
+ if not printer.getController().isPreheatRequestInProgress():
+ printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
+ except KeyError:
+ # Older firmwares don't support preheating, so we need to fake it.
+ pass
+
+ head_position = result["heads"][0]["position"]
+ printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])
+
+ for index in range(0, self._number_of_extruders):
+ temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
+ extruder = printer.extruders[index]
+ extruder.updateTargetHotendTemperature(temperatures["target"])
+ extruder.updateHotendTemperature(temperatures["current"])
+
+ material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]
+
+ if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
+ # Find matching material (as we need to set brand, type & color)
+ containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
+ GUID=material_guid)
+ if containers:
+ color = containers[0].getMetaDataEntry("color_code")
+ brand = containers[0].getMetaDataEntry("brand")
+ material_type = containers[0].getMetaDataEntry("material")
+ name = containers[0].getName()
+ else:
+ # Unknown material.
+ color = "#00000000"
+ brand = "Unknown"
+ material_type = "Unknown"
+ name = "Unknown"
+ material = MaterialOutputModel(guid=material_guid, type=material_type,
+ brand=brand, color=color, name = name)
+ extruder.updateActiveMaterial(material)
+
+ try:
+ hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
+ except KeyError:
+ hotend_id = ""
+ printer.extruders[index].updateHotendID(hotend_id)
+
+ else:
+ Logger.log("w",
+ "Got status code {status_code} while trying to get printer data".format(status_code = status_code))
+
+ ## Convenience function to "blur" out all but the last 5 characters of the auth key.
+ # This can be used to debug print the key, without it compromising the security.
+ def _getSafeAuthKey(self):
+ if self._authentication_key is not None:
+ result = self._authentication_key[-5:]
+ result = "********" + result
+ return result
+
+ return self._authentication_key
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py
new file mode 100644
index 0000000000..7a0e113d5b
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
+from PyQt5.QtCore import QTimer
+from UM.Version import Version
+
+MYPY = False
+if MYPY:
+ from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
+ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+
+
+class LegacyUM3PrinterOutputController(PrinterOutputController):
+ def __init__(self, output_device):
+ super().__init__(output_device)
+ self._preheat_bed_timer = QTimer()
+ self._preheat_bed_timer.setSingleShot(True)
+ self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
+ self._preheat_printer = None
+
+ self.can_control_manually = False
+
+ # Are we still waiting for a response about preheat?
+ # We need this so we can already update buttons, so it feels more snappy.
+ self._preheat_request_in_progress = False
+
+ def isPreheatRequestInProgress(self):
+ return self._preheat_request_in_progress
+
+ def setJobState(self, job: "PrintJobOutputModel", state: str):
+ data = "{\"target\": \"%s\"}" % state
+ self._output_device.put("print_job/state", data, onFinished=None)
+
+ def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
+ data = str(temperature)
+ self._output_device.put("printer/bed/temperature/target", data, onFinished=self._onPutBedTemperatureCompleted)
+
+ def _onPutBedTemperatureCompleted(self, reply):
+ if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
+ # If it was handling a preheat, it isn't anymore.
+ self._preheat_request_in_progress = False
+
+ def _onPutPreheatBedCompleted(self, reply):
+ self._preheat_request_in_progress = False
+
+ def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
+ head_pos = printer._head_position
+ new_x = head_pos.x + x
+ new_y = head_pos.y + y
+ new_z = head_pos.z + z
+ data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z)
+ self._output_device.put("printer/heads/0/position", data, onFinished=None)
+
+ def homeBed(self, printer):
+ self._output_device.put("printer/heads/0/position/z", "0", onFinished=None)
+
+ def _onPreheatBedTimerFinished(self):
+ self.setTargetBedTemperature(self._preheat_printer, 0)
+ self._preheat_printer.updateIsPreheating(False)
+ self._preheat_request_in_progress = True
+
+ def cancelPreheatBed(self, printer: "PrinterOutputModel"):
+ self.preheatBed(printer, temperature=0, duration=0)
+ self._preheat_bed_timer.stop()
+ printer.updateIsPreheating(False)
+
+ def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
+ try:
+ temperature = round(temperature) # The API doesn't allow floating point.
+ duration = round(duration)
+ except ValueError:
+ return # Got invalid values, can't pre-heat.
+
+ if duration > 0:
+ data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration)
+ else:
+ data = """{"temperature": "%i"}""" % temperature
+
+ # Real bed pre-heating support is implemented from 3.5.92 and up.
+
+ if Version(printer.firmwareVersion) < Version("3.5.92"):
+ # No firmware-side duration support then, so just set target bed temp and set a timer.
+ self.setTargetBedTemperature(printer, temperature=temperature)
+ self._preheat_bed_timer.setInterval(duration * 1000)
+ self._preheat_bed_timer.start()
+ self._preheat_printer = printer
+ printer.updateIsPreheating(True)
+ return
+
+ self._output_device.put("printer/bed/pre_heat", data, onFinished = self._onPutPreheatBedCompleted)
+ printer.updateIsPreheating(True)
+ self._preheat_request_in_progress = True
+
+
diff --git a/plugins/UM3NetworkPrinting/MonitorItem.qml b/plugins/UM3NetworkPrinting/MonitorItem.qml
index f69df41dd4..bbbc3feee6 100644
--- a/plugins/UM3NetworkPrinting/MonitorItem.qml
+++ b/plugins/UM3NetworkPrinting/MonitorItem.qml
@@ -6,40 +6,49 @@ import Cura 1.0 as Cura
Component
{
- Image
+ Item
{
- id: cameraImage
- property bool proportionalHeight:
+ width: maximumWidth
+ height: maximumHeight
+ Image
{
- if(sourceSize.height == 0 || maximumHeight == 0)
+ id: cameraImage
+ width: Math.min(sourceSize.width === 0 ? 800 * screenScaleFactor : sourceSize.width, maximumWidth)
+ height: Math.floor((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ z: 1
+ Component.onCompleted:
{
- return true;
+ if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
+ {
+ OutputDevice.activePrinter.camera.start()
+ }
}
- return (sourceSize.width / sourceSize.height) > (maximumWidth / maximumHeight);
- }
- property real _width: Math.floor(Math.min(maximumWidth, sourceSize.width))
- property real _height: Math.floor(Math.min(maximumHeight, sourceSize.height))
- width: proportionalHeight ? _width : Math.floor(sourceSize.width * _height / sourceSize.height)
- height: !proportionalHeight ? _height : Math.floor(sourceSize.height * _width / sourceSize.width)
- anchors.horizontalCenter: parent.horizontalCenter
-
- onVisibleChanged:
- {
- if(visible)
+ onVisibleChanged:
{
- OutputDevice.startCamera()
- } else
- {
- OutputDevice.stopCamera()
+ if(visible)
+ {
+ if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
+ {
+ OutputDevice.activePrinter.camera.start()
+ }
+ } else
+ {
+ if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
+ {
+ OutputDevice.activePrinter.camera.stop()
+ }
+ }
}
- }
- source:
- {
- if(OutputDevice.cameraImage)
+ source:
{
- return OutputDevice.cameraImage;
+ if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
+ {
+ return OutputDevice.activePrinter.camera.latestImage;
+ }
+ return "";
}
- return "";
}
}
}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py
deleted file mode 100644
index 05069d1c0d..0000000000
--- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py
+++ /dev/null
@@ -1,730 +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]
-
- # the build plates to be sent
- gcodes = getattr(Application.getInstance().getController().getScene(), "gcode_list")
- self._job_list = list(gcodes.keys())
- Logger.log("d", "build plates to be sent to printer: %s", (self._job_list))
-
- if self._stage != OutputStage.ready:
- if self._error_message:
- self._error_message.hide()
- self._error_message = Message(
- i18n_catalog.i18nc("@info:status",
- "Sending new jobs (temporarily) blocked, still sending the previous print job."))
- self._error_message.show()
- return
-
- self._add_build_plate_number = len(self._job_list) > 1
- self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
- if len(self._printers) > 1:
- self.spawnPrintView() # Ask user how to print it.
- elif len(self._printers) == 1:
- # If there is only one printer, don't bother asking.
- self.selectAutomaticPrinter()
- self.sendPrintJob()
-
- else:
- # Cluster has no printers, warn the user of this.
- if self._error_message:
- self._error_message.hide()
- self._error_message = Message(
- i18n_catalog.i18nc("@info:status",
- "Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers."))
- self._error_message.show()
-
- ## Actually send the print job, called from the dialog
- # :param: require_printer_name: name of printer, or ""
- @pyqtSlot()
- def sendPrintJob(self):
- nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job
- output_build_plate_number = self._job_list.pop(0)
- gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[output_build_plate_number]
-
- self._send_gcode_start = time.time()
- Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number))
-
- if self._stage != OutputStage.ready:
- Logger.log("d", "Unable to send print job as the state is %s", self._stage)
- raise OutputDeviceError.DeviceBusyError()
- self._stage = OutputStage.uploading
-
- if self._add_build_plate_number:
- self._file_name = "%s_%d.gcode.gz" % (file_name, output_build_plate_number)
- else:
- self._file_name = "%s.gcode.gz" % (file_name)
- self._showProgressMessage()
-
- require_printer_name = self._selected_printer["unique_name"]
-
- new_request = self._buildSendPrintJobHttpRequest(require_printer_name, gcode)
- if new_request is None or self._stage != OutputStage.uploading:
- return
- self._request = new_request
- self._reply = self._manager.post(self._request, self._multipart)
- self._reply.uploadProgress.connect(self._onUploadProgress)
- # See _finishedPrintJobPostRequest()
-
- def _buildSendPrintJobHttpRequest(self, require_printer_name, gcode):
- api_url = QUrl(self._api_base_uri + "print_jobs/")
- request = QNetworkRequest(api_url)
- # Create multipart request and add the g-code.
- self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType)
-
- # Add gcode
- part = QHttpPart()
- part.setHeader(QNetworkRequest.ContentDispositionHeader,
- 'form-data; name="file"; filename="%s"' % (self._file_name))
-
- compressed_gcode = self._compressGcode(gcode)
- 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()
-
- if self._job_list: # start sending next job
- self.sendPrintJob()
-
- def _showRequestFailedMessage(self, reply):
- if reply is not None:
- Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format(
- cluster_name = self.getName(),
- error_string = str(reply.errorString()),
- error = str(reply.error())))
- error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.")
- message = Message(text=error_message_template.format(
- cluster_name = self.getName()))
- message.show()
-
- def _showRequestSucceededMessage(self):
- confirmation_message_template = i18n_catalog.i18nc(
- "@info:status",
- "Sent {file_name} to group {cluster_name}."
- )
- file_name = os.path.basename(self._file_name).split(".")[0]
- message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
- message = Message(text=message_text)
- button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
- button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
- message.addAction("open_browser", button_text, "globe", button_tooltip)
- message.actionTriggered.connect(self._onMessageActionTriggered)
- message.show()
-
- def setPrintJobs(self, print_jobs):
- #TODO: hack, last seen messes up the check, so drop it.
- for job in print_jobs:
- del job["last_seen"]
- # Strip any extensions
- job["name"] = self._removeGcodeExtension(job["name"])
-
- if self._print_jobs != print_jobs:
- old_print_jobs = self._print_jobs
- self._print_jobs = print_jobs
-
- self._notifyFinishedPrintJobs(old_print_jobs, print_jobs)
- self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs)
-
- # Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer
- # for some reason. ugh.
- self._print_job_by_printer_uuid = {}
- self._print_job_by_uuid = {}
- for print_job in print_jobs:
- if "printer_uuid" in print_job and print_job["printer_uuid"] is not None:
- self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job
- self._print_job_by_uuid[print_job["uuid"]] = print_job
- self.printJobsChanged.emit()
-
- def _removeGcodeExtension(self, name):
- parts = name.split(".")
- if parts[-1].upper() == "GZ":
- parts = parts[:-1]
- if parts[-1].upper() == "GCODE":
- parts = parts[:-1]
- return ".".join(parts)
-
- def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs):
- """Notify the user when any of their print jobs have just completed.
-
- Arguments:
-
- old_print_jobs -- the previous list of print job status information as returned by the cluster REST API.
- new_print_jobs -- the current list of print job status information as returned by the cluster REST API.
- """
- if old_print_jobs is None:
- return
-
- username = self.__get_username()
- if username is None:
- return
-
- our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs)
- our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"]
-
- our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs)
- our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"]
-
- old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs])
-
- for print_job in our_new_finished_print_jobs:
- if print_job["uuid"] in old_not_finished_print_job_uuids:
-
- printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
- if printer_name is None:
- printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")
-
- message_text = (i18n_catalog.i18nc("@info:status",
- "Printer '{printer_name}' has finished printing '{job_name}'.")
- .format(printer_name=printer_name, job_name=print_job["name"]))
- message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished"))
- Application.getInstance().showMessage(message)
- Application.getInstance().showToastMessage(
- i18n_catalog.i18nc("@info:status", "Print finished"),
- message_text)
-
- def __filterOurPrintJobs(self, print_jobs):
- username = self.__get_username()
- return [print_job for print_job in print_jobs if print_job["owner"] == username]
-
- def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs):
- if old_print_jobs is None:
- return
-
- old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs))
- new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs))
- old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs])
-
- for print_job in new_change_required_print_jobs:
- if print_job["uuid"] not in old_change_required_print_job_uuids:
-
- printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"])
- if printer_name is None:
- # don't report on yet unknown printers
- continue
-
- message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.")
- .format(printer_name=printer_name, job_name=print_job["name"]))
- message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required"))
- Application.getInstance().showMessage(message)
- Application.getInstance().showToastMessage(
- i18n_catalog.i18nc("@label:status", "Action required"),
- message_text)
-
- def __filterConfigChangePrintJobs(self, print_jobs):
- return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs)
-
- def __isConfigurationChangeRequiredPrintJob(self, print_job):
- if print_job["status"] == "queued":
- changes_required = print_job.get("configuration_changes_required", [])
- return len(changes_required) != 0
- return False
-
- def __getPrinterNameFromUuid(self, printer_uuid):
- for printer in self._printers:
- if printer["uuid"] == printer_uuid:
- return printer["friendly_name"]
- return None
-
- def setPrinters(self, printers):
- if self._printers != printers:
- self._connected_printers_type_count = []
- printers_count = {}
- self._printers = printers
- self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name
-
- for printer in printers:
- variant = printer["machine_variant"]
- if variant in printers_count:
- printers_count[variant] += 1
- else:
- printers_count[variant] = 1
- for type in printers_count:
- self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]})
- self.printersChanged.emit()
-
- @pyqtProperty("QVariantList", notify=printersChanged)
- def connectedPrintersTypeCount(self):
- return self._connected_printers_type_count
-
- @pyqtProperty("QVariantList", notify=printersChanged)
- def connectedPrinters(self):
- return self._printers
-
- @pyqtProperty(int, notify=printJobsChanged)
- def numJobsPrinting(self):
- num_jobs_printing = 0
- for job in self._print_jobs:
- if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]:
- num_jobs_printing += 1
- return num_jobs_printing
-
- @pyqtProperty(int, notify=printJobsChanged)
- def numJobsQueued(self):
- num_jobs_queued = 0
- for job in self._print_jobs:
- if job["status"] == "queued":
- num_jobs_queued += 1
- return num_jobs_queued
-
- @pyqtProperty("QVariantMap", notify=printJobsChanged)
- def printJobsByUUID(self):
- return self._print_job_by_uuid
-
- @pyqtProperty("QVariantMap", notify=printJobsChanged)
- def printJobsByPrinterUUID(self):
- return self._print_job_by_printer_uuid
-
- @pyqtProperty("QVariantList", notify=printJobsChanged)
- def printJobs(self):
- return self._print_jobs
-
- @pyqtProperty("QVariantList", notify=printersChanged)
- def printers(self):
- return [self._automatic_printer, ] + self._printers
-
- @pyqtSlot(str, str)
- def selectPrinter(self, unique_name, friendly_name):
- self.stopCamera()
- self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name}
- Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name)
- # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
- if unique_name == "":
- self._address = self._master_address
- else:
- self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"]
-
- self.selectedPrinterChanged.emit()
-
- def _updateJobState(self, job_state):
- name = self._selected_printer.get("friendly_name")
- if name == "" or name == "Automatic":
- # TODO: This is now a bit hacked; If no printer is selected, don't show job state.
- if self._job_state != "":
- self._job_state = ""
- self.jobStateChanged.emit()
- else:
- if self._job_state != job_state:
- self._job_state = job_state
- self.jobStateChanged.emit()
-
- @pyqtSlot()
- def selectAutomaticPrinter(self):
- self.stopCamera()
- self._selected_printer = self._automatic_printer
- self.selectedPrinterChanged.emit()
-
- @pyqtProperty("QVariant", notify=selectedPrinterChanged)
- def selectedPrinterName(self):
- return self._selected_printer.get("unique_name", "")
-
- def getPrintJobsUrl(self):
- return self._host + "/print_jobs"
-
- def getPrintersUrl(self):
- return self._host + "/printers"
-
- def _showProgressMessage(self):
- progress_message_template = i18n_catalog.i18nc("@info:progress",
- "Sending {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 de0a06527e..0000000000
--- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py
+++ /dev/null
@@ -1,1308 +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)
- active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
- self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[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.
-
- # 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().findInstanceContainersMetadata(type = "material", GUID = remote_material_guid, read_only = True)
- remote_material_name = "Unknown"
- if remote_materials:
- remote_material_name = remote_materials[0]["name"]
- 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):
- registry = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance()
- for container in registry.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 registry.isReadOnly(container.getId()):
- 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 0d3ed52f03..0000000000
--- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py
+++ /dev/null
@@ -1,361 +0,0 @@
-# Copyright (c) 2017 Ultimaker B.V.
-# Cura is released under the terms of the LGPLv3 or higher.
-
-import time
-import json
-from queue import Queue
-from threading import Event, Thread
-
-from PyQt5.QtCore import QObject, pyqtSlot
-from PyQt5.QtCore import QUrl
-from PyQt5.QtGui import QDesktopServices
-from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
-from UM.Application import Application
-from UM.Logger import Logger
-from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
-from UM.Preferences import Preferences
-from UM.Signal import Signal, signalemitter
-from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo # type: ignore
-
-from . import NetworkPrinterOutputDevice, NetworkClusterPrinterOutputDevice
-
-
-## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
-# Zero-Conf is used to detect printers, which are saved in a dict.
-# If we discover a printer that has the same key as the active machine instance a connection is made.
-@signalemitter
-class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
- def __init__(self):
- super().__init__()
- self._zero_conf = None
- self._browser = None
- self._printers = {}
- self._cluster_printers_seen = {} # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer
-
- self._api_version = "1"
- self._api_prefix = "/api/v" + self._api_version + "/"
- self._cluster_api_version = "1"
- self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
-
- self._network_manager = QNetworkAccessManager()
- self._network_manager.finished.connect(self._onNetworkRequestFinished)
-
- # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
- # authentication requests.
- self._old_printers = []
- self._excluded_addresses = ["127.0.0.1"] # Adding a list of not allowed IP addresses. At this moment, just localhost
-
- # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
- self.addPrinterSignal.connect(self.addPrinter)
- self.removePrinterSignal.connect(self.removePrinter)
- Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
-
- # Get list of manual printers from preferences
- self._preferences = Preferences.getInstance()
- self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames
- self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
-
- self._network_requests_buffer = {} # store api responses until data is complete
-
- # The zeroconf service changed requests are handled in a separate thread, so we can re-schedule the requests
- # which fail to get detailed service info.
- # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
- # them up and process them.
- self._service_changed_request_queue = Queue()
- self._service_changed_request_event = Event()
- self._service_changed_request_thread = Thread(target = self._handleOnServiceChangedRequests,
- daemon = True)
- self._service_changed_request_thread.start()
-
- addPrinterSignal = Signal()
- removePrinterSignal = Signal()
- printerListChanged = Signal()
-
- ## Start looking for devices on network.
- def start(self):
- self.startDiscovery()
-
- def startDiscovery(self):
- self.stop()
- if self._browser:
- self._browser.cancel()
- self._browser = None
- self._old_printers = [printer_name for printer_name in self._printers]
- self._printers = {}
- self.printerListChanged.emit()
- # After network switching, one must make a new instance of Zeroconf
- # On windows, the instance creation is very fast (unnoticable). Other platforms?
- self._zero_conf = Zeroconf()
- self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest])
-
- # Look for manual instances from preference
- for address in self._manual_instances:
- if address:
- self.addManualPrinter(address)
-
- def addManualPrinter(self, address):
- if address not in self._manual_instances:
- self._manual_instances.append(address)
- self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
-
- instance_name = "manual:%s" % address
- properties = {
- b"name": address.encode("utf-8"),
- b"address": address.encode("utf-8"),
- b"manual": b"true",
- b"incomplete": b"true"
- }
-
- if instance_name not in self._printers:
- # Add a preliminary printer instance
- self.addPrinter(instance_name, address, properties)
-
- self.checkManualPrinter(address)
- self.checkClusterPrinter(address)
-
- def removeManualPrinter(self, key, address = None):
- if key in self._printers:
- if not address:
- address = self._printers[key].ipAddress
- self.removePrinter(key)
-
- if address in self._manual_instances:
- self._manual_instances.remove(address)
- self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
-
- def checkManualPrinter(self, address):
- # Check if a printer exists at this address
- # If a printer responds, it will replace the preliminary printer created above
- # origin=manual is for tracking back the origin of the call
- url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name")
- name_request = QNetworkRequest(url)
- self._network_manager.get(name_request)
-
- def checkClusterPrinter(self, address):
- cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster")
- cluster_request = QNetworkRequest(cluster_url)
- self._network_manager.get(cluster_request)
-
- ## Handler for all requests that have finished.
- def _onNetworkRequestFinished(self, reply):
- reply_url = reply.url().toString()
- status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
-
- if reply.operation() == QNetworkAccessManager.GetOperation:
- address = reply.url().host()
- if "origin=manual_name" in reply_url: # Name returned from printer.
- if status_code == 200:
-
- try:
- system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
- except json.JSONDecodeError:
- Logger.log("e", "Printer returned invalid JSON.")
- return
- except UnicodeDecodeError:
- Logger.log("e", "Printer returned incorrect UTF-8.")
- return
-
- if address not in self._network_requests_buffer:
- self._network_requests_buffer[address] = {}
- self._network_requests_buffer[address]["system"] = system_info
- elif "origin=check_cluster" in reply_url:
- if address not in self._network_requests_buffer:
- self._network_requests_buffer[address] = {}
- if status_code == 200:
- # We know it's a cluster printer
- Logger.log("d", "Cluster printer detected: [%s]", reply.url())
-
- try:
- cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
- except json.JSONDecodeError:
- Logger.log("e", "Printer returned invalid JSON.")
- return
- except UnicodeDecodeError:
- Logger.log("e", "Printer returned incorrect UTF-8.")
- return
-
- self._network_requests_buffer[address]["cluster"] = True
- self._network_requests_buffer[address]["cluster_size"] = len(cluster_printers_list)
- else:
- Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url())
- self._network_requests_buffer[address]["cluster"] = False
-
- # Both the system call and cluster call are finished
- if (address in self._network_requests_buffer and
- "system" in self._network_requests_buffer[address] and
- "cluster" in self._network_requests_buffer[address]):
-
- instance_name = "manual:%s" % address
- system_info = self._network_requests_buffer[address]["system"]
- machine = "unknown"
- if "variant" in system_info:
- variant = system_info["variant"]
- if variant == "Ultimaker 3":
- machine = "9066"
- elif variant == "Ultimaker 3 Extended":
- machine = "9511"
-
- properties = {
- b"name": system_info["name"].encode("utf-8"),
- b"address": address.encode("utf-8"),
- b"firmware_version": system_info["firmware"].encode("utf-8"),
- b"manual": b"true",
- b"machine": machine.encode("utf-8")
- }
-
- if self._network_requests_buffer[address]["cluster"]:
- properties[b"cluster_size"] = self._network_requests_buffer[address]["cluster_size"]
-
- if instance_name in self._printers:
- # Only replace the printer if it is still in the list of (manual) printers
- self.removePrinter(instance_name)
- self.addPrinter(instance_name, address, properties)
-
- del self._network_requests_buffer[address]
-
- ## Stop looking for devices on network.
- def stop(self):
- if self._zero_conf is not None:
- Logger.log("d", "zeroconf close...")
- self._zero_conf.close()
-
- def getPrinters(self):
- return self._printers
-
- def reCheckConnections(self):
- active_machine = Application.getInstance().getGlobalContainerStack()
- if not active_machine:
- return
-
- for key in self._printers:
- if key == active_machine.getMetaDataEntry("um_network_key"):
- if not self._printers[key].isConnected():
- Logger.log("d", "Connecting [%s]..." % key)
- self._printers[key].connect()
- self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
- else:
- if self._printers[key].isConnected():
- Logger.log("d", "Closing connection [%s]..." % key)
- self._printers[key].close()
- self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
-
- ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
- def addPrinter(self, name, address, properties):
- cluster_size = int(properties.get(b"cluster_size", -1))
- if cluster_size >= 0:
- printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice(
- name, address, properties, self._api_prefix)
- else:
- printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
- self._printers[printer.getKey()] = printer
- self._cluster_printers_seen[printer.getKey()] = name # Cluster printers that may be temporary unreachable or is rebooted keep being stored here
- global_container_stack = Application.getInstance().getGlobalContainerStack()
- if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
- if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced?
- Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey())
- self._printers[printer.getKey()].connect()
- printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
- self.printerListChanged.emit()
-
- def removePrinter(self, name):
- printer = self._printers.pop(name, None)
- if printer:
- if printer.isConnected():
- printer.disconnect()
- printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
- Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
- self.printerListChanged.emit()
-
- ## Handler for when the connection state of one of the detected printers changes
- def _onPrinterConnectionStateChanged(self, key):
- if key not in self._printers:
- return
- if self._printers[key].isConnected():
- self.getOutputDeviceManager().addOutputDevice(self._printers[key])
- else:
- self.getOutputDeviceManager().removeOutputDevice(key)
-
- ## Handler for zeroConf detection.
- # Return True or False indicating if the process succeeded.
- def _onServiceChanged(self, zeroconf, service_type, name, state_change):
- if state_change == ServiceStateChange.Added:
- Logger.log("d", "Bonjour service added: %s" % name)
-
- # First try getting info from zeroconf cache
- info = ServiceInfo(service_type, name, properties = {})
- for record in zeroconf.cache.entries_with_name(name.lower()):
- info.update_record(zeroconf, time.time(), record)
-
- for record in zeroconf.cache.entries_with_name(info.server):
- info.update_record(zeroconf, time.time(), record)
- if info.address:
- break
-
- # Request more data if info is not complete
- if not info.address:
- Logger.log("d", "Trying to get address of %s", name)
- info = zeroconf.get_service_info(service_type, name)
-
- if info:
- type_of_device = info.properties.get(b"type", None)
- if type_of_device:
- if type_of_device == b"printer":
- address = '.'.join(map(lambda n: str(n), info.address))
- if address in self._excluded_addresses:
- Logger.log("d", "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.", address, name)
- return False # When getting the localhost IP, then try to reconnect
- self.addPrinterSignal.emit(str(name), address, info.properties)
- else:
- Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device )
- else:
- Logger.log("w", "Could not get information about %s" % name)
- return False
-
- elif state_change == ServiceStateChange.Removed:
- Logger.log("d", "Bonjour service removed: %s" % name)
- self.removePrinterSignal.emit(str(name))
-
- return True
-
- ## Appends a service changed request so later the handling thread will pick it up and processes it.
- def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
- # append the request and set the event so the event handling thread can pick it up
- item = (zeroconf, service_type, name, state_change)
- self._service_changed_request_queue.put(item)
- self._service_changed_request_event.set()
-
- def _handleOnServiceChangedRequests(self):
- while True:
- # wait for the event to be set
- self._service_changed_request_event.wait(timeout = 5.0)
- # stop if the application is shutting down
- if Application.getInstance().isShuttingDown():
- return
-
- self._service_changed_request_event.clear()
-
- # handle all pending requests
- reschedule_requests = [] # a list of requests that have failed so later they will get re-scheduled
- while not self._service_changed_request_queue.empty():
- request = self._service_changed_request_queue.get()
- zeroconf, service_type, name, state_change = request
- try:
- result = self._onServiceChanged(zeroconf, service_type, name, state_change)
- if not result:
- reschedule_requests.append(request)
- except Exception:
- Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
- service_type, name)
- reschedule_requests.append(request)
-
- # re-schedule the failed requests if any
- if reschedule_requests:
- for request in reschedule_requests:
- self._service_changed_request_queue.put(request)
-
- @pyqtSlot()
- def openControlPanel(self):
- Logger.log("d", "Opening print jobs web UI...")
- selected_device = self.getOutputDeviceManager().getActiveDevice()
- if isinstance(selected_device, NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice):
- QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl()))
diff --git a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml
index 03ff4542e1..70fa65da5e 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 != null ? printCoreConfiguration.activeMaterial.name : ""
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
@@ -23,7 +23,7 @@ Item
Label
{
id: printCoreLabel
- text: printCoreConfiguration.print_core_id
+ text: printCoreConfiguration.hotendID
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width
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
}
}
diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml
index c253ebae89..6d7d6c8a7d 100644
--- a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml
+++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml
@@ -22,16 +22,16 @@ Rectangle
{
return "";
}
- if (printJob.time_total === 0)
+ if (printJob.timeTotal === 0)
{
return "";
}
- return Math.min(100, Math.round(printJob.time_elapsed / printJob.time_total * 100)) + "%";
+ return Math.min(100, Math.round(printJob.timeElapsed / printJob.timeTotal * 100)) + "%";
}
function printerStatusText(printer)
{
- switch (printer.status)
+ switch (printer.state)
{
case "pre_print":
return catalog.i18nc("@label", "Preparing to print")
@@ -49,31 +49,23 @@ Rectangle
}
id: printerDelegate
- property var printer
+
+ property var printer: null
+ property var printJob: printer != null ? printer.activePrintJob: null
border.width: UM.Theme.getSize("default_lining").width
border.color: mouse.containsMouse ? emphasisColor : lineColor
z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible.
- property var printJob:
- {
- if (printer.reserved_by != null)
- {
- // Look in another list.
- return OutputDevice.printJobsByUUID[printer.reserved_by]
- }
- return OutputDevice.printJobsByPrinterUUID[printer.uuid]
- }
-
MouseArea
{
id: mouse
anchors.fill:parent
- onClicked: OutputDevice.selectPrinter(printer.unique_name, printer.friendly_name)
+ onClicked: OutputDevice.setActivePrinter(printer)
hoverEnabled: true;
// Only clickable if no printer is selected
- enabled: OutputDevice.selectedPrinterName == "" && printer.status !== "unreachable"
+ enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
}
Row
@@ -122,7 +114,7 @@ Rectangle
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
- text: printJob != null ? getPrettyTime(printJob.time_total) : ""
+ text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
opacity: 0.65
font: UM.Theme.getFont("default")
elide: Text.ElideRight
@@ -140,7 +132,7 @@ Rectangle
anchors.top: parent.top
anchors.left: parent.left
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
- text: printer.friendly_name
+ text: printer.name
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
@@ -150,7 +142,7 @@ Rectangle
id: printerTypeLabel
anchors.top: printerNameLabel.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
- text: printer.machine_variant
+ text: printer.type
anchors.left: parent.left
elide: Text.ElideRight
font: UM.Theme.getFont("very_small")
@@ -166,7 +158,7 @@ Rectangle
anchors.right: printProgressArea.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: emphasisColor
- opacity: printer != null && printer.status === "unreachable" ? 0.3 : 1
+ opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
Image
{
@@ -192,7 +184,7 @@ Rectangle
{
id: leftExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
- printCoreConfiguration: printer.configuration[0]
+ printCoreConfiguration: printer.extruders[0]
}
Rectangle
@@ -207,7 +199,7 @@ Rectangle
{
id: rightExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
- printCoreConfiguration: printer.configuration[1]
+ printCoreConfiguration: printer.extruders[1]
}
}
@@ -225,9 +217,9 @@ Rectangle
if(printJob != null)
{
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"];
- return extendStates.indexOf(printJob.status) !== -1;
+ return extendStates.indexOf(printJob.state) !== -1;
}
- return !printer.enabled;
+ return printer.state == "disabled"
}
Item // Status and Percent
@@ -235,7 +227,7 @@ Rectangle
id: printProgressTitleBar
property var showPercent: {
- return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.status) !== -1);
+ return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.state) !== -1);
}
width: parent.width
@@ -252,19 +244,19 @@ Rectangle
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
text: {
- if (!printer.enabled)
+ if (printer.state == "disabled")
{
return catalog.i18nc("@label:status", "Disabled");
}
- if (printer.status === "unreachable")
+ if (printer.state === "unreachable")
{
return printerStatusText(printer);
}
if (printJob != null)
{
- switch (printJob.status)
+ switch (printJob.state)
{
case "printing":
case "post_print":
@@ -277,14 +269,7 @@ Rectangle
case "sent_to_printer":
return catalog.i18nc("@label", "Preparing to print")
case "queued":
- if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
- {
return catalog.i18nc("@label:status", "Action required");
- }
- else
- {
- return "";
- }
case "pausing":
case "paused":
return catalog.i18nc("@label:status", "Paused");
@@ -328,26 +313,23 @@ Rectangle
visible: !printProgressTitleBar.showPercent
source: {
- if (!printer.enabled)
+ if (printer.state == "disabled")
{
return "blocked-icon.svg";
}
- if (printer.status === "unreachable")
+ if (printer.state === "unreachable")
{
return "";
}
if (printJob != null)
{
- if(printJob.status === "queued")
+ if(printJob.state === "queued")
{
- if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
- {
- return "action-required-icon.svg";
- }
+ return "action-required-icon.svg";
}
- else if (printJob.status === "wait_cleanup")
+ else if (printJob.state === "wait_cleanup")
{
return "checkmark-icon.svg";
}
@@ -384,23 +366,23 @@ Rectangle
{
text:
{
- if (!printer.enabled)
+ if (printer.state == "disabled")
{
return catalog.i18nc("@label", "Not accepting print jobs");
}
- if (printer.status === "unreachable")
+ if (printer.state === "unreachable")
{
return "";
}
if(printJob != null)
{
- switch (printJob.status)
+ switch (printJob.state)
{
case "printing":
case "post_print":
- return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.time_total - printJob.time_elapsed)
+ return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.timeTotal - printJob.timeElapsed)
case "wait_cleanup":
return catalog.i18nc("@label", "Clear build plate")
case "sent_to_printer":
@@ -409,10 +391,7 @@ Rectangle
case "wait_for_configuration":
return catalog.i18nc("@label", "Not accepting print jobs")
case "queued":
- if (printJob.configuration_changes_required != undefined)
- {
- return catalog.i18nc("@label", "Waiting for configuration change");
- }
+ return catalog.i18nc("@label", "Waiting for configuration change");
default:
return "";
}
@@ -432,7 +411,7 @@ Rectangle
text: {
if(printJob != null)
{
- if(printJob.status == "printing" || printJob.status == "post_print")
+ if(printJob.state == "printing" || printJob.state == "post_print")
{
return OutputDevice.getDateCompleted(printJob.time_total - printJob.time_elapsed)
}
diff --git a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml
index 6793d74ac5..3e6f6a8fd8 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
}
@@ -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 "";
}
diff --git a/plugins/UM3NetworkPrinting/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/UM3InfoComponents.qml
index d0c95e1524..18b481a6ed 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
{
@@ -115,22 +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() {
- 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;
- }
}
}
diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
new file mode 100644
index 0000000000..c639c25007
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
@@ -0,0 +1,332 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
+from UM.Logger import Logger
+from UM.Application import Application
+from UM.Signal import Signal, signalemitter
+from UM.Preferences import Preferences
+from UM.Version import Version
+
+from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
+
+from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
+from PyQt5.QtCore import QUrl
+
+from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
+from queue import Queue
+from threading import Event, Thread
+from time import time
+
+import json
+
+
+## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
+# Zero-Conf is used to detect printers, which are saved in a dict.
+# If we discover a printer that has the same key as the active machine instance a connection is made.
+@signalemitter
+class UM3OutputDevicePlugin(OutputDevicePlugin):
+ addDeviceSignal = Signal()
+ removeDeviceSignal = Signal()
+ discoveredDevicesChanged = Signal()
+
+ def __init__(self):
+ super().__init__()
+ self._zero_conf = None
+ self._zero_conf_browser = None
+
+ # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
+ self.addDeviceSignal.connect(self._onAddDevice)
+ self.removeDeviceSignal.connect(self._onRemoveDevice)
+
+ Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
+
+ self._discovered_devices = {}
+
+ self._network_manager = QNetworkAccessManager()
+ self._network_manager.finished.connect(self._onNetworkRequestFinished)
+
+ self._min_cluster_version = Version("4.0.0")
+
+ self._api_version = "1"
+ self._api_prefix = "/api/v" + self._api_version + "/"
+ self._cluster_api_version = "1"
+ self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
+
+ # Get list of manual instances from preferences
+ self._preferences = Preferences.getInstance()
+ self._preferences.addPreference("um3networkprinting/manual_instances",
+ "") # A comma-separated list of ip adresses or hostnames
+
+ self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
+
+ # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
+ # which fail to get detailed service info.
+ # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
+ # them up and process them.
+ self._service_changed_request_queue = Queue()
+ self._service_changed_request_event = Event()
+ self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
+ self._service_changed_request_thread.start()
+
+ def getDiscoveredDevices(self):
+ return self._discovered_devices
+
+ ## Start looking for devices on network.
+ def start(self):
+ self.startDiscovery()
+
+ def startDiscovery(self):
+ self.stop()
+ if self._zero_conf_browser:
+ self._zero_conf_browser.cancel()
+ self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed.
+
+ self._zero_conf = Zeroconf()
+ self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
+ [self._appendServiceChangedRequest])
+
+ # Look for manual instances from preference
+ for address in self._manual_instances:
+ if address:
+ self.addManualDevice(address)
+
+ def reCheckConnections(self):
+ active_machine = Application.getInstance().getGlobalContainerStack()
+ if not active_machine:
+ return
+
+ um_network_key = active_machine.getMetaDataEntry("um_network_key")
+
+ for key in self._discovered_devices:
+ if key == um_network_key:
+ if not self._discovered_devices[key].isConnected():
+ Logger.log("d", "Attempting to connect with [%s]" % key)
+ self._discovered_devices[key].connect()
+ self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
+ else:
+ if self._discovered_devices[key].isConnected():
+ Logger.log("d", "Attempting to close connection with [%s]" % key)
+ self._discovered_devices[key].close()
+ self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
+
+ def _onDeviceConnectionStateChanged(self, key):
+ if key not in self._discovered_devices:
+ return
+ if self._discovered_devices[key].isConnected():
+ self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
+ else:
+ self.getOutputDeviceManager().removeOutputDevice(key)
+
+ def stop(self):
+ if self._zero_conf is not None:
+ Logger.log("d", "zeroconf close...")
+ self._zero_conf.close()
+
+ def removeManualDevice(self, key, address = None):
+ if key in self._discovered_devices:
+ if not address:
+ address = self._printers[key].ipAddress
+ self._onRemoveDevice(key)
+
+ if address in self._manual_instances:
+ self._manual_instances.remove(address)
+ self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
+
+ def addManualDevice(self, address):
+ if address not in self._manual_instances:
+ self._manual_instances.append(address)
+ self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
+
+ instance_name = "manual:%s" % address
+ properties = {
+ b"name": address.encode("utf-8"),
+ b"address": address.encode("utf-8"),
+ b"manual": b"true",
+ b"incomplete": b"true"
+ }
+
+ if instance_name not in self._discovered_devices:
+ # Add a preliminary printer instance
+ self._onAddDevice(instance_name, address, properties)
+
+ self._checkManualDevice(address)
+
+ def _checkManualDevice(self, address):
+ # Check if a UM3 family device exists at this address.
+ # If a printer responds, it will replace the preliminary printer created above
+ # origin=manual is for tracking back the origin of the call
+ url = QUrl("http://" + address + self._api_prefix + "system")
+ name_request = QNetworkRequest(url)
+ self._network_manager.get(name_request)
+
+ def _onNetworkRequestFinished(self, reply):
+ reply_url = reply.url().toString()
+
+ if "system" in reply_url:
+ if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
+ # Something went wrong with checking the firmware version!
+ return
+
+ try:
+ system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
+ except:
+ Logger.log("e", "Something went wrong converting the JSON.")
+ return
+
+ address = reply.url().host()
+ has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
+ instance_name = "manual:%s" % address
+ properties = {
+ b"name": system_info["name"].encode("utf-8"),
+ b"address": address.encode("utf-8"),
+ b"firmware_version": system_info["firmware"].encode("utf-8"),
+ b"manual": b"true",
+ b"machine": system_info["variant"].encode("utf-8")
+ }
+
+ if has_cluster_capable_firmware:
+ # Cluster needs an additional request, before it's completed.
+ properties[b"incomplete"] = b"true"
+
+ # Check if the device is still in the list & re-add it with the updated
+ # information.
+ if instance_name in self._discovered_devices:
+ self._onRemoveDevice(instance_name)
+ self._onAddDevice(instance_name, address, properties)
+
+ if has_cluster_capable_firmware:
+ # We need to request more info in order to figure out the size of the cluster.
+ cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
+ cluster_request = QNetworkRequest(cluster_url)
+ self._network_manager.get(cluster_request)
+
+ elif "printers" in reply_url:
+ if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
+ # Something went wrong with checking the amount of printers the cluster has!
+ return
+ # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
+ try:
+ cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
+ except:
+ Logger.log("e", "Something went wrong converting the JSON.")
+ return
+ address = reply.url().host()
+ instance_name = "manual:%s" % address
+ if instance_name in self._discovered_devices:
+ device = self._discovered_devices[instance_name]
+ properties = device.getProperties().copy()
+ if b"incomplete" in properties:
+ del properties[b"incomplete"]
+ properties[b'cluster_size'] = len(cluster_printers_list)
+ self._onRemoveDevice(instance_name)
+ self._onAddDevice(instance_name, address, properties)
+
+ def _onRemoveDevice(self, device_id):
+ device = self._discovered_devices.pop(device_id, None)
+ if device:
+ if device.isConnected():
+ device.disconnect()
+ try:
+ device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
+ except TypeError:
+ # Disconnect already happened.
+ pass
+
+ self.discoveredDevicesChanged.emit()
+
+ def _onAddDevice(self, name, address, properties):
+ # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
+ # or "Legacy" UM3 device.
+ cluster_size = int(properties.get(b"cluster_size", -1))
+
+ if cluster_size >= 0:
+ device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
+ else:
+ device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)
+
+ self._discovered_devices[device.getId()] = device
+ self.discoveredDevicesChanged.emit()
+
+ global_container_stack = Application.getInstance().getGlobalContainerStack()
+ if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
+ device.connect()
+ device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
+
+ ## Appends a service changed request so later the handling thread will pick it up and processes it.
+ def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
+ # append the request and set the event so the event handling thread can pick it up
+ item = (zeroconf, service_type, name, state_change)
+ self._service_changed_request_queue.put(item)
+ self._service_changed_request_event.set()
+
+ def _handleOnServiceChangedRequests(self):
+ while True:
+ # Wait for the event to be set
+ self._service_changed_request_event.wait(timeout = 5.0)
+
+ # Stop if the application is shutting down
+ if Application.getInstance().isShuttingDown():
+ return
+
+ self._service_changed_request_event.clear()
+
+ # Handle all pending requests
+ reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled
+ while not self._service_changed_request_queue.empty():
+ request = self._service_changed_request_queue.get()
+ zeroconf, service_type, name, state_change = request
+ try:
+ result = self._onServiceChanged(zeroconf, service_type, name, state_change)
+ if not result:
+ reschedule_requests.append(request)
+ except Exception:
+ Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
+ service_type, name)
+ reschedule_requests.append(request)
+
+ # Re-schedule the failed requests if any
+ if reschedule_requests:
+ for request in reschedule_requests:
+ self._service_changed_request_queue.put(request)
+
+ ## Handler for zeroConf detection.
+ # Return True or False indicating if the process succeeded.
+ # Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread.
+ def _onServiceChanged(self, zero_conf, service_type, name, state_change):
+ if state_change == ServiceStateChange.Added:
+ Logger.log("d", "Bonjour service added: %s" % name)
+
+ # First try getting info from zero-conf cache
+ info = ServiceInfo(service_type, name, properties={})
+ for record in zero_conf.cache.entries_with_name(name.lower()):
+ info.update_record(zero_conf, time(), record)
+
+ for record in zero_conf.cache.entries_with_name(info.server):
+ info.update_record(zero_conf, time(), record)
+ if info.address:
+ break
+
+ # Request more data if info is not complete
+ if not info.address:
+ Logger.log("d", "Trying to get address of %s", name)
+ info = zero_conf.get_service_info(service_type, name)
+
+ if info:
+ type_of_device = info.properties.get(b"type", None)
+ if type_of_device:
+ if type_of_device == b"printer":
+ address = '.'.join(map(lambda n: str(n), info.address))
+ self.addDeviceSignal.emit(str(name), address, info.properties)
+ else:
+ Logger.log("w",
+ "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
+ else:
+ Logger.log("w", "Could not get information about %s" % name)
+ return False
+
+ elif state_change == ServiceStateChange.Removed:
+ Logger.log("d", "Bonjour service removed: %s" % name)
+ self.removeDeviceSignal.emit(str(name))
+
+ return True
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py
index 37f863bd00..b68086cb75 100644
--- a/plugins/UM3NetworkPrinting/__init__.py
+++ b/plugins/UM3NetworkPrinting/__init__.py
@@ -1,12 +1,14 @@
-# Copyright (c) 2015 Ultimaker B.V.
+# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-from . import NetworkPrinterOutputDevicePlugin
+
from . import DiscoverUM3Action
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
+from . import UM3OutputDevicePlugin
+
def getMetaData():
return {}
def register(app):
- return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
\ No newline at end of file
+ return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
\ No newline at end of file
diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py
new file mode 100644
index 0000000000..72f4f20262
--- /dev/null
+++ b/plugins/USBPrinting/AutoDetectBaudJob.py
@@ -0,0 +1,66 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from UM.Job import Job
+from UM.Logger import Logger
+
+from .avr_isp.stk500v2 import Stk500v2
+
+from time import time, sleep
+from serial import Serial, SerialException
+
+
+# An async job that attempts to find the correct baud rate for a USB printer.
+# It tries a pre-set list of baud rates. All these baud rates are validated by requesting the temperature a few times
+# and checking if the results make sense. If getResult() is not None, it was able to find a correct baud rate.
+class AutoDetectBaudJob(Job):
+ def __init__(self, serial_port):
+ super().__init__()
+ self._serial_port = serial_port
+ self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
+
+ def run(self):
+ Logger.log("d", "Auto detect baud rate started.")
+ timeout = 3
+
+ programmer = Stk500v2()
+ serial = None
+ try:
+ programmer.connect(self._serial_port)
+ serial = programmer.leaveISP()
+ except:
+ programmer.close()
+
+ for baud_rate in self._all_baud_rates:
+ Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
+
+ if serial is None:
+ try:
+ serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout)
+ except SerialException as e:
+ Logger.logException("w", "Unable to create serial")
+ continue
+ else:
+ # We already have a serial connection, just change the baud rate.
+ try:
+ serial.baudrate = baud_rate
+ except:
+ continue
+ sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
+ successful_responses = 0
+
+ serial.write(b"\n") # Ensure we clear out previous responses
+ serial.write(b"M105\n")
+
+ timeout_time = time() + timeout
+
+ while timeout_time > time():
+ line = serial.readline()
+ if b"ok T:" in line:
+ successful_responses += 1
+ if successful_responses >= 3:
+ self.setResult(baud_rate)
+ return
+
+ serial.write(b"M105\n")
+ self.setResult(None) # Unable to detect the correct baudrate.
diff --git a/plugins/USBPrinting/FirmwareUpdateWindow.qml b/plugins/USBPrinting/FirmwareUpdateWindow.qml
index 44218b61b1..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
@@ -34,44 +34,22 @@ UM.Dialog
}
text: {
- if (manager.errorCode == 0)
+ switch (manager.firmwareUpdateState)
{
- if (manager.firmwareUpdateCompleteStatus)
- {
- //: Firmware update status label
- return catalog.i18nc("@label","Firmware update completed.")
- }
- else if (manager.progress == 0)
- {
- //: Firmware update status label
- return catalog.i18nc("@label","Starting firmware update, this may take a while.")
- }
- else
- {
- //: Firmware update status label
+ case 0:
+ return "" //Not doing anything (eg; idling)
+ case 1:
return catalog.i18nc("@label","Updating firmware.")
- }
- }
- else
- {
- switch (manager.errorCode)
- {
- case 1:
- //: Firmware update status label
- return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
- case 2:
- //: Firmware update status label
- return catalog.i18nc("@label","Firmware update failed due to an communication error.")
- case 3:
- //: Firmware update status label
- return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
- case 4:
- //: Firmware update status label
- return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
- default:
- //: Firmware update status label
- return catalog.i18nc("@label", "Unknown error code: %1").arg(manager.errorCode)
- }
+ case 2:
+ return catalog.i18nc("@label","Firmware update completed.")
+ case 3:
+ return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
+ case 4:
+ return catalog.i18nc("@label","Firmware update failed due to an communication error.")
+ case 5:
+ return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
+ case 6:
+ return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
}
}
@@ -81,16 +59,15 @@ UM.Dialog
ProgressBar
{
id: prog
- value: manager.firmwareUpdateCompleteStatus ? 100 : manager.progress
+ value: manager.firmwareProgress
minimumValue: 0
maximumValue: 100
- indeterminate: (manager.progress < 1) && (!manager.firmwareUpdateCompleteStatus)
+ indeterminate: manager.firmwareProgress < 1 && manager.firmwareProgress > 0
anchors
{
left: parent.left;
right: parent.right;
}
-
}
SystemPalette
diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/plugins/USBPrinting/USBPrinterOutputController.py
new file mode 100644
index 0000000000..ba45e7b0ca
--- /dev/null
+++ b/plugins/USBPrinting/USBPrinterOutputController.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
+from PyQt5.QtCore import QTimer
+
+MYPY = False
+if MYPY:
+ from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
+ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+
+
+class USBPrinterOuptutController(PrinterOutputController):
+ def __init__(self, output_device):
+ super().__init__(output_device)
+
+ self._preheat_bed_timer = QTimer()
+ self._preheat_bed_timer.setSingleShot(True)
+ self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
+ self._preheat_printer = None
+
+ def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
+ self._output_device.sendCommand("G91")
+ self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
+ self._output_device.sendCommand("G90")
+
+ def homeHead(self, printer):
+ self._output_device.sendCommand("G28 X")
+ self._output_device.sendCommand("G28 Y")
+
+ def homeBed(self, printer):
+ self._output_device.sendCommand("G28 Z")
+
+ def setJobState(self, job: "PrintJobOutputModel", state: str):
+ if state == "pause":
+ self._output_device.pausePrint()
+ job.updateState("paused")
+ elif state == "print":
+ self._output_device.resumePrint()
+ job.updateState("printing")
+ elif state == "abort":
+ self._output_device.cancelPrint()
+ pass
+
+ def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
+ try:
+ temperature = round(temperature) # The API doesn't allow floating point.
+ duration = round(duration)
+ except ValueError:
+ return # Got invalid values, can't pre-heat.
+
+ self.setTargetBedTemperature(printer, temperature=temperature)
+ self._preheat_bed_timer.setInterval(duration * 1000)
+ self._preheat_bed_timer.start()
+ self._preheat_printer = printer
+ printer.updateIsPreheating(True)
+
+ def cancelPreheatBed(self, printer: "PrinterOutputModel"):
+ self.preheatBed(printer, temperature=0, duration=0)
+ self._preheat_bed_timer.stop()
+ printer.updateIsPreheating(False)
+
+ def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
+ self._output_device.sendCommand("M140 S%s" % temperature)
+
+ def _onPreheatBedTimerFinished(self):
+ self.setTargetBedTemperature(self._preheat_printer, 0)
+ self._preheat_printer.updateIsPreheating(False)
\ No newline at end of file
diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py
index 1930f5402b..6c03450a88 100644
--- a/plugins/USBPrinting/USBPrinterOutputDevice.py
+++ b/plugins/USBPrinting/USBPrinterOutputDevice.py
@@ -1,472 +1,87 @@
# 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 UM.Qt.Duration import DurationFormat
+from UM.PluginRegistry import PluginRegistry
+
+from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
+from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+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, sleep
+from queue import Queue
+from enum import IntEnum
+from typing import Union, Optional, List
+
+import re
+import functools # Used for reduce
+import os
+
catalog = i18nCatalog("cura")
class USBPrinterOutputDevice(PrinterOutputDevice):
- def __init__(self, serial_port):
+ firmwareProgressChanged = pyqtSignal()
+ firmwareUpdateStateChanged = pyqtSignal()
+
+ 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.setConnectionText(catalog.i18nc("@info:status", "Connected via USB"))
- self._serial = None
+ self._serial = None # type: Optional[Serial]
self._serial_port = serial_port
- self._error_state = None
+ self._address = serial_port
- self._connect_thread = threading.Thread(target = self._connect)
- self._connect_thread.daemon = True
-
- self._end_stop_thread = None
- self._poll_endstop = False
-
- # 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._listen_thread = threading.Thread(target=self._listen)
- self._listen_thread.daemon = True
-
- self._update_firmware_thread = threading.Thread(target= self._updateFirmware)
- self._update_firmware_thread.daemon = True
- self.firmwareUpdateComplete.connect(self._onFirmwareUpdateComplete)
-
- self._heatup_wait_start_time = time.time()
-
- 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
+ self._timeout = 3
# 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 = [] # type: List[str]
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._use_auto_detect = True
- self.writeFinished.emit(self)
+ self._baud_rate = baud_rate
- ## Get the serial port string of this connection.
- # \return serial port
- def getSerialPort(self):
- return self._serial_port
+ self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
- ## 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()
+ # 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)
- ## 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
+ self._update_firmware_thread = Thread(target=self._updateFirmware, daemon = True)
- if self._connection_state != ConnectionState.closed:
- self.close()
- hex_file = intelHex.readHex(self._firmware_file_name)
+ self._last_temperature_request = None # type: Optional[int]
- if len(hex_file) == 0:
- Logger.log("e", "Unable to read provided hex file. Could not update firmware")
- self._updateFirmwareFailedMissingFirmware()
- return
+ self._is_printing = False # A print is being sent.
- programmer = stk500v2.Stk500v2()
- programmer.progress_callback = self.setProgress
+ ## Set when print is started in order to check running time.
+ self._print_start_time = None # type: Optional[int]
+ self._print_estimated_time = None # type: Optional[int]
- try:
- programmer.connect(self._serial_port)
- except Exception:
- programmer.close()
- pass
+ self._accepts_commands = True
- # Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
- time.sleep(1)
+ self._paused = False
- if not programmer.isConnected():
- Logger.log("e", "Unable to connect with serial. Could not update firmware")
- self._updateFirmwareFailedCommunicationError()
- return
+ self._firmware_view = None
+ self._firmware_location = None
+ self._firmware_progress = 0
+ self._firmware_update_state = FirmwareUpdateState.idle
- 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
-
- ## Close the printer connection
- def close(self):
- Logger.log("d", "Closing the USB printer connection.")
- if self._connect_thread.isAlive():
- 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._connect_thread = threading.Thread(target = self._connect)
- self._connect_thread.daemon = True
-
- 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()
-
- 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):
- if self._serial is None:
- return
-
- if "M109" in cmd or "M190" in cmd:
- self._heatup_wait_start_time = time.time()
-
- 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()
-
- ## 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)
-
- ## 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()
+ # 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.
#
@@ -476,277 +91,295 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# 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))
+ if self._is_printing:
+ return # Aleady printing
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
+ # find the G-code for the active build plate to print
+ 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]
- ## Listen thread function.
- def _listen(self):
- Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
+ 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: List[str]):
+ self._gcode.clear()
+ self._paused = False
+
+ 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: 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: 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
+
+ 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._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()
- 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
+ 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()
- 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
+ def close(self):
+ super().close()
+ if self._serial is not None:
+ self._serial.close()
- 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()
+ # Re-create the thread so it can be started again later.
+ self._update_thread = Thread(target=self._update, daemon=True)
+ self._serial = None
- # 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:])
+ ## 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)
- 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)
+ def _sendCommand(self, command: Union[str, bytes]):
+ if self._serial is None:
+ return
- 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 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)
+
+ def _update(self):
+ while self._connection_state == ConnectionState.connected and self._serial is not None:
+ 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")
+ self._last_temperature_request = time()
+
+ 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):
+ 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)
+ 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 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
+ elif self._paused:
+ pass # Nothing to do!
else:
self._sendNextGcodeLine()
- elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with "resend" and "rs"
+ elif b"resend" in line.lower() or b"rs" in line:
+ # A resend can be requested either by Resend, resend or 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])
+ 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])
- # 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")
+ def pausePrint(self):
+ self._paused = True
- Logger.log("i", "Printer connection listen thread stopped for %s" % self._serial_port)
+ def resumePrint(self):
+ self._paused = False
- ## 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 = []
+ 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.homeHead()
+ self.printers[0].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
+ 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]
- ## 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
+ if ";" in line:
+ line = line[:line.find(";")]
- ## 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
+ line = line.strip()
- def _onFirmwareUpdateComplete(self):
- self._update_firmware_thread.join()
- self._update_firmware_thread = threading.Thread(target = self._updateFirmware)
- self._update_firmware_thread.daemon = True
+ # 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"
- self.connect()
+ checksum = functools.reduce(lambda x, y: x ^ y, map(ord, "N%d%s" % (self._gcode_position, line)))
- ## 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()
+ self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
- ## 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()
+ 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 = USBPrinterOuptutController(self), name= Application.getInstance().getPrintInformation().jobName)
+ print_job.updateState("printing")
+ 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
+
+
+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 62412bb521..58b6106fb0 100644
--- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
+++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
@@ -2,33 +2,32 @@
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Signal import Signal, signalemitter
-from . import USBPrinterOutputDevice
from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
-from UM.PluginRegistry import PluginRegistry
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
-from cura.PrinterOutputDevice import ConnectionState
-from UM.Qt.ListModel import ListModel
-from UM.Message import Message
+from UM.i18n import i18nCatalog
+from cura.PrinterOutputDevice import ConnectionState
from cura.CuraApplication import CuraApplication
+from . import USBPrinterOutputDevice
+from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
+
import threading
import platform
import time
-import os.path
import serial.tools.list_ports
-from UM.Extension import Extension
-from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
-from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
-## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer.
+## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.
@signalemitter
-class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
+class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
+ addUSBOutputDeviceSignal = Signal()
+ progressChanged = pyqtSignal()
+
def __init__(self, parent = None):
super().__init__(parent = parent)
self._serial_port_list = []
@@ -38,39 +37,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
self._update_thread.setDaemon(True)
self._check_updates = True
- self._firmware_view = None
Application.getInstance().applicationShuttingDown.connect(self.stop)
- self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
-
- addUSBOutputDeviceSignal = Signal()
- connectionStateChanged = pyqtSignal()
-
- progressChanged = pyqtSignal()
- firmwareUpdateChange = pyqtSignal()
-
- @pyqtProperty(float, notify = progressChanged)
- def progress(self):
- progress = 0
- for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
- progress += device.progress
- return progress / len(self._usb_output_devices)
-
- @pyqtProperty(int, notify = progressChanged)
- def errorCode(self):
- for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
- if device._error_code:
- return device._error_code
- return 0
-
- ## Return True if all printers finished firmware update
- @pyqtProperty(float, notify = firmwareUpdateChange)
- def firmwareUpdateCompleteStatus(self):
- complete = True
- for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
- if not device.firmwareUpdateFinished:
- complete = False
- return complete
+ # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
+ self.addUSBOutputDeviceSignal.connect(self.addOutputDevice)
def start(self):
self._check_updates = True
@@ -79,58 +49,28 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
def stop(self):
self._check_updates = False
- def _updateThread(self):
- while self._check_updates:
- result = self.getSerialPortList(only_list_usb = True)
- self._addRemovePorts(result)
- time.sleep(5)
-
- ## Show firmware interface.
- # This will create the view if its not already created.
- def spawnFirmwareInterface(self, serial_port):
- if self._firmware_view is None:
- path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
- self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self})
-
- self._firmware_view.show()
-
- @pyqtSlot(str)
- def updateAllFirmware(self, file_name):
- if file_name.startswith("file://"):
- file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
-
- if not self._usb_output_devices:
- Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
+ def _onConnectionStateChanged(self, serial_port):
+ if serial_port not in self._usb_output_devices:
return
- for printer_connection in self._usb_output_devices:
- self._usb_output_devices[printer_connection].resetFirmwareUpdate()
- self.spawnFirmwareInterface("")
- for printer_connection in self._usb_output_devices:
- try:
- self._usb_output_devices[printer_connection].updateFirmware(file_name)
- except FileNotFoundError:
- # Should only happen in dev environments where the resources/firmware folder is absent.
- self._usb_output_devices[printer_connection].setProgress(100, 100)
- Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name)
- Message(i18n_catalog.i18nc("@info",
- "Could not find firmware required for the printer at %s.") % printer_connection, title = i18n_catalog.i18nc("@info:title", "Printer Firmware")).show()
- self._firmware_view.close()
+ changed_device = self._usb_output_devices[serial_port]
+ if changed_device.connectionState == ConnectionState.connected:
+ self.getOutputDeviceManager().addOutputDevice(changed_device)
+ else:
+ self.getOutputDeviceManager().removeOutputDevice(serial_port)
+ def _updateThread(self):
+ while self._check_updates:
+ container_stack = Application.getInstance().getGlobalContainerStack()
+ if container_stack is None:
+ time.sleep(5)
continue
-
- @pyqtSlot(str, str, result = bool)
- def updateFirmwareBySerial(self, serial_port, file_name):
- if serial_port in self._usb_output_devices:
- self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort())
- try:
- self._usb_output_devices[serial_port].updateFirmware(file_name)
- except FileNotFoundError:
- self._firmware_view.close()
- Logger.log("e", "Could not find firmware required for this machine called '%s'", file_name)
- return False
- return True
- return False
+ if container_stack.getMetaDataEntry("supports_usb_connection"):
+ port_list = self.getSerialPortList(only_list_usb=True)
+ else:
+ port_list = [] # Just use an empty list; all USB devices will be removed.
+ self._addRemovePorts(port_list)
+ time.sleep(5)
## Return the singleton instance of the USBPrinterManager
@classmethod
@@ -191,7 +131,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
Logger.log("w", "There is no firmware for machine %s.", machine_id)
if hex_file:
- return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
+ try:
+ return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
+ except FileNotFoundError:
+ Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
+ return ""
else:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
@@ -205,46 +149,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
continue
self._serial_port_list = list(serial_ports)
- devices_to_remove = []
for port, device in self._usb_output_devices.items():
if port not in self._serial_port_list:
device.close()
- devices_to_remove.append(port)
-
- for port in devices_to_remove:
- del self._usb_output_devices[port]
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addOutputDevice(self, serial_port):
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
device.connectionStateChanged.connect(self._onConnectionStateChanged)
- device.connect()
- device.progressChanged.connect(self.progressChanged)
- device.firmwareUpdateChange.connect(self.firmwareUpdateChange)
self._usb_output_devices[serial_port] = device
-
- ## If one of the states of the connected devices change, we might need to add / remove them from the global list.
- def _onConnectionStateChanged(self, serial_port):
- success = True
- try:
- if self._usb_output_devices[serial_port].connectionState == ConnectionState.connected:
- self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port])
- else:
- success = success and self.getOutputDeviceManager().removeOutputDevice(serial_port)
- if success:
- self.connectionStateChanged.emit()
- except KeyError:
- Logger.log("w", "Connection state of %s changed, but it was not found in the list")
-
- @pyqtProperty(QObject , notify = connectionStateChanged)
- def connectedPrinterList(self):
- self._usb_output_devices_model = ListModel()
- self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name")
- self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer")
- for connection in self._usb_output_devices:
- if self._usb_output_devices[connection].connectionState == ConnectionState.connected:
- self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]})
- return self._usb_output_devices_model
+ device.connect()
## Create a list of serial ports on the system.
# \param only_list_usb If true, only usb ports are listed
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()}
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
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()
]}
diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json
index 6eef6b1e9b..87b72928ca 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,
@@ -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",
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..9aae3a5244
--- /dev/null
+++ b/resources/definitions/malyan_m200.def.json
@@ -0,0 +1,85 @@
+{
+ "id": "malyan_m200",
+ "version": 2,
+ "name": "Malyan M200",
+ "inherits": "fdmprinter",
+ "metadata": {
+ "author": "Brian Corbino, Tyler Gibson",
+ "manufacturer": "Malyan",
+ "category": "Other",
+ "file_formats": "text/x-gcode",
+ "platform": "malyan_m200_platform.stl",
+ "has_machine_quality": true,
+ "has_materials": true,
+ "preferred_quality": "*normal*",
+ "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": "min(material_print_temperature + 5, 245)" },
+ "material_bed_temperature": { "minimum_value": "0" },
+ "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)" },
+ "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..7264f0a6fc
--- /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, Tyler Gibson",
+ "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..87014c136b
--- /dev/null
+++ b/resources/definitions/monoprice_select_mini_v2.def.json
@@ -0,0 +1,25 @@
+{
+ "id": "monoprice_select_mini_v2",
+ "version": 2,
+ "name": "Monoprice Select Mini V2 (E3D)",
+ "inherits": "malyan_m200",
+ "metadata": {
+ "author": "Tyler Gibson",
+ "manufacturer": "Monoprice",
+ "category": "Other",
+ "file_formats": "text/x-gcode",
+ "has_machine_quality": true,
+ "has_materials": true,
+ "preferred_quality": "*normal*",
+ "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},
+ "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }
+ }
+}
diff --git a/resources/meshes/malyan_m200_platform.stl b/resources/meshes/malyan_m200_platform.stl
new file mode 100644
index 0000000000..32b19a0911
Binary files /dev/null and b/resources/meshes/malyan_m200_platform.stl differ
diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml
index c130cf041b..86f46ef7a1 100644
--- a/resources/qml/Cura.qml
+++ b/resources/qml/Cura.qml
@@ -395,6 +395,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
@@ -455,29 +478,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
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);
diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml
index b23ba4c95a..0e9728da3d 100644
--- a/resources/qml/MonitorButton.qml
+++ b/resources/qml/MonitorButton.qml
@@ -17,15 +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":
@@ -46,18 +70,23 @@ Item
property variant statusColor:
{
- if(!printerConnected || !printerAcceptsCommands)
+ if(!printerConnected || !printerAcceptsCommands || activePrinter == null)
+ {
return UM.Theme.getColor("text");
+ }
- switch(Cura.MachineManager.printerOutputDevices[0].printerState)
+ switch(activePrinter.state)
{
case "maintenance":
return UM.Theme.getColor("status_busy");
case "error":
return UM.Theme.getColor("status_stopped");
}
-
- switch(Cura.MachineManager.printerOutputDevices[0].jobState)
+ if(base.activePrintJob == null)
+ {
+ return UM.Theme.getColor("text");
+ }
+ switch(base.activePrintJob.state)
{
case "printing":
case "pre_print":
@@ -84,17 +113,30 @@ 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 == null)
+ {
+ return "";
+ }
+ if(activePrinter.state == "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");
@@ -162,7 +204,11 @@ Item
{
return false;
}
- switch(Cura.MachineManager.printerOutputDevices[0].jobState)
+ if(base.activePrintJob == null)
+ {
+ return false
+ }
+ switch(base.activePrintJob.state)
{
case "pausing":
case "resuming":
@@ -184,7 +230,8 @@ Item
anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width;
}
- Row {
+ Row
+ {
id: buttonsRow
height: abortButton.height
anchors.top: progressBar.bottom
@@ -193,7 +240,8 @@ 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
}
@@ -224,19 +272,17 @@ Item
property bool userClicked: false
property string lastJobState: ""
- visible: printerConnected && Cura.MachineManager.printerOutputDevices[0].canPause
- enabled: (!userClicked) && printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands &&
- (["paused", "printing"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0)
+ visible: printerConnected && activePrinter != null &&activePrinter.canPause
+ enabled: (!userClicked) && printerConnected && printerAcceptsCommands && activePrintJob != null &&
+ (["paused", "printing"].indexOf(activePrintJob.state) >= 0)
text: {
- var result = "";
- if (!printerConnected)
+ if (!printerConnected || activePrintJob == null)
{
- return "";
+ return catalog.i18nc("@label:", "Pause");
}
- var jobState = Cura.MachineManager.printerOutputDevices[0].jobState;
- if (jobState == "paused")
+ if (activePrintJob.state == "paused")
{
return catalog.i18nc("@label:", "Resume");
}
@@ -247,14 +293,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");
}
}
@@ -265,9 +314,9 @@ Item
{
id: abortButton
- visible: printerConnected && Cura.MachineManager.printerOutputDevices[0].canAbort
- enabled: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands &&
- (["paused", "printing", "pre_print"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0)
+ visible: printerConnected && activePrinter != null && activePrinter.canAbort
+ enabled: printerConnected && printerAcceptsCommands && activePrintJob != null &&
+ (["paused", "printing", "pre_print"].indexOf(activePrintJob.state) >= 0)
height: UM.Theme.getSize("save_button_save_to_button").height
@@ -286,7 +335,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")
}
}
}
diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml
index a02ea2288d..489e38e8d7 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;
}
@@ -139,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
}
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)
+ }
+ }
+
+
}
}
}
diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml
index 311150c6b9..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,23 +91,17 @@ 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;
+ 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 +111,29 @@ 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
- {
+
+ // 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 +403,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
}
}
@@ -435,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 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 {
diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml
index 5a5c160b51..471729192e 100644
--- a/resources/qml/PrintMonitor.qml
+++ b/resources/qml/PrintMonitor.qml
@@ -9,10 +9,14 @@ import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
+import "PrinterOutput"
+
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
+ property var activePrintJob: activePrinter != null ? activePrinter.activePrintJob: null
Cura.ExtrudersModel
{
@@ -20,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
@@ -76,189 +45,13 @@ Column
Repeater
{
id: extrudersRepeater
- model: machineExtruderCount.properties.value
+ model: activePrinter!=null ? activePrinter.extruders : null
- 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: modelData
}
}
}
@@ -271,391 +64,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
@@ -677,498 +95,53 @@ Column
watchedProperties: ["value"]
}
- Column
+ ManualPrinterControl
{
- visible: connectedPrinter != null ? connectedPrinter.canControlManually : false
- enabled:
- {
- if (connectedPrinter == null)
- {
- return false; //Can't control the printer if not connected.
- }
- 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")
- {
- 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: connectedPrinter != null ? connectedPrinter.jobName : ""
- }
- Loader
- {
- sourceComponent: monitorItem
- property string label: catalog.i18nc("@label", "Printing Time")
- property string value: connectedPrinter != null ? getPrettyTime(connectedPrinter.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")
+ label: catalog.i18nc("@label", "Active print")
+ width: base.width
+ visible: activePrinter != null
}
- Component
- {
- id: monitorItem
- Row
+ MonitorItem
+ {
+ label: catalog.i18nc("@label", "Job Name")
+ value: activePrintJob != null ? activePrintJob.name : ""
+ width: base.width
+ visible: activePrinter != null
+ }
+
+ MonitorItem
+ {
+ label: catalog.i18nc("@label", "Printing Time")
+ value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal) : ""
+ width:base.width
+ visible: activePrinter != null
+ }
+
+ MonitorItem
+ {
+ label: catalog.i18nc("@label", "Estimated time left")
+ value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal - activePrintJob.timeElapsed) : ""
+ visible:
{
- 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
+ if(activePrintJob == null)
+ {
+ return false
+ }
- 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
- }
- }
+ return (activePrintJob.state == "printing" ||
+ activePrintJob.state == "resuming" ||
+ activePrintJob.state == "pausing" ||
+ activePrintJob.state == "paused")
}
+ width: base.width
}
}
diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml
new file mode 100644
index 0000000000..a7141262a9
--- /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.type : ""
+ 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/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml
new file mode 100644
index 0000000000..bc89da2251
--- /dev/null
+++ b/resources/qml/PrinterOutput/HeatedBedBox.qml
@@ -0,0 +1,352 @@
+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
+ }
+ 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;
+ }
+ }
+ }
+ }
+
+ 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 (printerModel.isPreheating)
+ {
+ return true;
+ }
+ 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:
+ {
+ if(printerModel == null)
+ {
+ return ""
+ }
+ if(printerModel.isPreheating )
+ {
+ return catalog.i18nc("@button Cancel pre-heating", "Cancel")
+ } else
+ {
+ return catalog.i18nc("@button", "Pre-heat")
+ }
+ }
+ }
+ }
+ }
+
+ onClicked:
+ {
+ if (!printerModel.isPreheating)
+ {
+ printerModel.preheatBed(preheatTemperatureInput.text, 900);
+ }
+ else
+ {
+ printerModel.cancelPreheatBed();
+ }
+ }
+
+ 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
diff --git a/resources/qml/PrinterOutput/ManualPrinterControl.qml b/resources/qml/PrinterOutput/ManualPrinterControl.qml
new file mode 100644
index 0000000000..43fa769fb5
--- /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 (!connectedDevice.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:
+ {
+ printerModel.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:
+ {
+ printerModel.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:
+ {
+ printerModel.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:
+ {
+ printerModel.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:
+ {
+ printerModel.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:
+ {
+ printerModel.moveHead(0, 0, distancesRow.currentDistance)
+ }
+ }
+
+ Button
+ {
+ iconSource: UM.Theme.getIcon("home");
+ style: monitorButtonStyle
+ width: height
+ height: UM.Theme.getSize("setting_control").height
+
+ onClicked:
+ {
+ printerModel.homeBed()
+ }
+ }
+
+ Button
+ {
+ iconSource: UM.Theme.getIcon("arrow_bottom");
+ style: monitorButtonStyle
+ width: height
+ height: UM.Theme.getSize("setting_control").height
+
+ onClicked:
+ {
+ printerModel.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
diff --git a/resources/qml/PrinterOutput/OutputDeviceHeader.qml b/resources/qml/PrinterOutput/OutputDeviceHeader.qml
new file mode 100644
index 0000000000..d5ce32786c
--- /dev/null
+++ b/resources/qml/PrinterOutput/OutputDeviceHeader.qml
@@ -0,0 +1,59 @@
+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")
+ property var activePrinter: outputDevice != null ? outputDevice.activePrinter : null
+
+ 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 ? activePrinter.name : ""
+ }
+
+ 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 ? "" : 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: parent.top
+ anchors.topMargin: UM.Theme.getSize("default_margin").height
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/Settings/SettingCategory.qml b/resources/qml/Settings/SettingCategory.qml
index cc7ca9354d..13576eb483 100644
--- a/resources/qml/Settings/SettingCategory.qml
+++ b/resources/qml/Settings/SettingCategory.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import UM 1.1 as UM
import Cura 1.0 as Cura
diff --git a/resources/qml/Settings/SettingCheckBox.qml b/resources/qml/Settings/SettingCheckBox.qml
index 2aad6181d8..c4d6a5c764 100644
--- a/resources/qml/Settings/SettingCheckBox.qml
+++ b/resources/qml/Settings/SettingCheckBox.qml
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
+import QtQuick 2.7
import QtQuick.Layouts 1.1
-import QtQuick.Controls 2.1
+import QtQuick.Controls 2.0
import UM 1.2 as UM
diff --git a/resources/qml/Settings/SettingComboBox.qml b/resources/qml/Settings/SettingComboBox.qml
index 63d13ae514..f8a60bc749 100644
--- a/resources/qml/Settings/SettingComboBox.qml
+++ b/resources/qml/Settings/SettingComboBox.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import UM 1.1 as UM
diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml
index b754c3fd74..eba6351e4d 100644
--- a/resources/qml/Settings/SettingExtruder.qml
+++ b/resources/qml/Settings/SettingExtruder.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2016 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import UM 1.1 as UM
import Cura 1.0 as Cura
diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml
index 1b1f302846..414f05d80e 100644
--- a/resources/qml/Settings/SettingItem.qml
+++ b/resources/qml/Settings/SettingItem.qml
@@ -1,9 +1,9 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
+import QtQuick 2.7
import QtQuick.Layouts 1.1
-import QtQuick.Controls 2.1
+import QtQuick.Controls 2.0
import UM 1.1 as UM
import Cura 1.0 as Cura
diff --git a/resources/qml/Settings/SettingOptionalExtruder.qml b/resources/qml/Settings/SettingOptionalExtruder.qml
index 3bb2a2f1e0..19f7c7f370 100644
--- a/resources/qml/Settings/SettingOptionalExtruder.qml
+++ b/resources/qml/Settings/SettingOptionalExtruder.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2016 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import UM 1.1 as UM
import Cura 1.0 as Cura
diff --git a/resources/qml/Settings/SettingTextField.qml b/resources/qml/Settings/SettingTextField.qml
index 181cb9cb77..176a4e23e6 100644
--- a/resources/qml/Settings/SettingTextField.qml
+++ b/resources/qml/Settings/SettingTextField.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import UM 1.1 as UM
diff --git a/resources/qml/Settings/SettingUnknown.qml b/resources/qml/Settings/SettingUnknown.qml
index 82d54f96b8..704964eda2 100644
--- a/resources/qml/Settings/SettingUnknown.qml
+++ b/resources/qml/Settings/SettingUnknown.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import UM 1.2 as UM
diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml
index 6929614830..08f4ec5a68 100644
--- a/resources/qml/Settings/SettingView.qml
+++ b/resources/qml/Settings/SettingView.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2017 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
+import QtQuick 2.7
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml
index e1c0a07f89..6a860fe32d 100644
--- a/resources/qml/Sidebar.qml
+++ b/resources/qml/Sidebar.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import UM 1.2 as UM
diff --git a/resources/qml/SidebarAdvanced.qml b/resources/qml/SidebarAdvanced.qml
index f214e425b1..ff5f545c80 100644
--- a/resources/qml/SidebarAdvanced.qml
+++ b/resources/qml/SidebarAdvanced.qml
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
-import QtQuick.Controls 2.1
+import QtQuick 2.7
+import QtQuick.Controls 2.0
import "Settings"
diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml
index ac124c8911..b96c40d9ea 100644
--- a/resources/qml/SidebarSimple.qml
+++ b/resources/qml/SidebarSimple.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.8
+import QtQuick 2.7
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
@@ -195,11 +195,22 @@ Item
text:
{
var result = ""
- if (Cura.MachineManager.activeMachine != null) {
+ if(Cura.MachineManager.activeMachine != null)
+ {
result = Cura.ProfilesModel.getItem(index).layer_height_without_unit
- if (result == undefined)
- result = ""
+ if(result == undefined)
+ {
+ 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
}
diff --git a/resources/qml/Topbar.qml b/resources/qml/Topbar.qml
index e3dfb85c49..dc041ab5b7 100644
--- a/resources/qml/Topbar.qml
+++ b/resources/qml/Topbar.qml
@@ -99,6 +99,7 @@ Rectangle
{
id: viewOrientationControl
height: 30
+
spacing: 2
visible: UM.Controller.activeStage.stageId != "MonitorStage"
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_0.04375.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg
new file mode 100644
index 0000000000..54be6ecbcc
--- /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 = 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_0.0875.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg
new file mode 100644
index 0000000000..568dd796f3
--- /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 = 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_0.13125.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg
new file mode 100644
index 0000000000..1dc436502b
--- /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 = 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_0.175.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg
new file mode 100644
index 0000000000..314a8acd83
--- /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 = 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_0.21875.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg
new file mode 100644
index 0000000000..a7fedb7e04
--- /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 = 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_0.2625.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg
new file mode 100644
index 0000000000..441abc3070
--- /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 = 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_0.30625.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg
new file mode 100644
index 0000000000..2588838174
--- /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 = 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/malyan_m200_0.35.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg
new file mode 100644
index 0000000000..800b6104d9
--- /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 = 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_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/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