mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-06 05:23:58 -06:00
commit
5164065f58
219 changed files with 9441 additions and 5686 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -47,7 +47,6 @@ plugins/Doodle3D-cura-plugin
|
|||
plugins/FlatProfileExporter
|
||||
plugins/GodMode
|
||||
plugins/OctoPrintPlugin
|
||||
plugins/PostProcessingPlugin
|
||||
plugins/ProfileFlattener
|
||||
plugins/X3GWriter
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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.
|
||||
|
|
|
@ -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", "€")
|
||||
|
@ -646,10 +647,10 @@ class CuraApplication(QtApplication):
|
|||
if parsed_args["help"]:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def run(self):
|
||||
self.preRun()
|
||||
|
||||
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
|
||||
|
||||
self._setUpSingleInstanceServer()
|
||||
|
@ -804,6 +805,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||
|
||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
|
||||
qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
|
||||
|
@ -890,12 +892,18 @@ class CuraApplication(QtApplication):
|
|||
def getSceneBoundingBoxString(self):
|
||||
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
|
||||
|
||||
## Update scene bounding box for current build plate
|
||||
def updatePlatformActivity(self, node = None):
|
||||
count = 0
|
||||
scene_bounding_box = None
|
||||
is_block_slicing_node = False
|
||||
active_build_plate = self.getBuildPlateModel().activeBuildPlate
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")):
|
||||
if (
|
||||
not issubclass(type(node), CuraSceneNode) or
|
||||
(not node.getMeshData() and not node.callDecoration("getLayerData")) or
|
||||
(node.callDecoration("getBuildPlateNumber") != active_build_plate)):
|
||||
|
||||
continue
|
||||
if node.callDecoration("isBlockSlicing"):
|
||||
is_block_slicing_node = True
|
||||
|
@ -915,7 +923,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()
|
||||
|
||||
|
@ -1012,7 +1020,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
Selection.clear()
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
|
@ -1020,7 +1028,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)
|
||||
|
@ -1034,10 +1042,12 @@ class CuraApplication(QtApplication):
|
|||
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) not in {SceneNode, CuraSceneNode}:
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if not node.isSelectable():
|
||||
continue # Only remove nodes that are selectable.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
nodes.append(node)
|
||||
|
@ -1050,7 +1060,11 @@ class CuraApplication(QtApplication):
|
|||
op.push()
|
||||
Selection.clear()
|
||||
|
||||
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||
Logger.log("i", "Reseting print information")
|
||||
self._print_information = PrintInformation.PrintInformation()
|
||||
|
||||
# stay on the same build plate
|
||||
#self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||
|
||||
## Reset all translation on nodes with mesh data.
|
||||
@pyqtSlot()
|
||||
|
@ -1058,7 +1072,7 @@ class CuraApplication(QtApplication):
|
|||
Logger.log("i", "Resetting all scene translations")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
|
@ -1086,13 +1100,13 @@ class CuraApplication(QtApplication):
|
|||
Logger.log("i", "Resetting all scene transformations")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
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)
|
||||
|
||||
|
@ -1113,13 +1127,13 @@ class CuraApplication(QtApplication):
|
|||
def arrangeObjectsToAllBuildPlates(self):
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
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:
|
||||
|
@ -1134,7 +1148,7 @@ class CuraApplication(QtApplication):
|
|||
nodes = []
|
||||
active_build_plate = self.getBuildPlateModel().activeBuildPlate
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
|
@ -1142,7 +1156,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
|
||||
|
@ -1158,7 +1172,7 @@ class CuraApplication(QtApplication):
|
|||
# What nodes are on the build plate and are not being moved
|
||||
fixed_nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
|
@ -1166,7 +1180,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
|
||||
|
@ -1186,7 +1200,7 @@ class CuraApplication(QtApplication):
|
|||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not issubclass(type(node), SceneNode) or not node.getMeshData():
|
||||
if not isinstance(node, SceneNode) or not node.getMeshData():
|
||||
continue
|
||||
|
||||
nodes.append(node)
|
||||
|
@ -1421,18 +1435,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)
|
||||
|
@ -1477,7 +1497,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()
|
||||
|
|
|
@ -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 isinstance(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,
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -61,7 +61,7 @@ class PlatformPhysics:
|
|||
|
||||
random.shuffle(nodes)
|
||||
for node in nodes:
|
||||
if node is root or not issubclass(type(node), SceneNode) or node.getBoundingBox() is None:
|
||||
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
|
||||
continue
|
||||
|
||||
bbox = node.getBoundingBox()
|
||||
|
|
|
@ -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:
|
||||
|
|
70
cura/PrinterOutput/ExtruderOuputModel.py
Normal file
70
cura/PrinterOutput/ExtruderOuputModel.py
Normal file
|
@ -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()
|
34
cura/PrinterOutput/MaterialOutputModel.py
Normal file
34
cura/PrinterOutput/MaterialOutputModel.py
Normal file
|
@ -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
|
119
cura/PrinterOutput/NetworkCamera.py
Normal file
119
cura/PrinterOutput/NetworkCamera.py
Normal file
|
@ -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()
|
304
cura/PrinterOutput/NetworkedPrinterOutputDevice.py
Normal file
304
cura/PrinterOutput/NetworkedPrinterOutputDevice.py
Normal file
|
@ -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
|
101
cura/PrinterOutput/PrintJobOutputModel.py
Normal file
101
cura/PrinterOutput/PrintJobOutputModel.py
Normal file
|
@ -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)
|
46
cura/PrinterOutput/PrinterOutputController.py
Normal file
46
cura/PrinterOutput/PrinterOutputController.py
Normal file
|
@ -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")
|
240
cura/PrinterOutput/PrinterOutputModel.py
Normal file
240
cura/PrinterOutput/PrinterOutputModel.py
Normal file
|
@ -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
|
0
cura/PrinterOutput/__init__.py
Normal file
0
cura/PrinterOutput/__init__.py
Normal file
|
@ -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,150 @@ 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()
|
||||
|
||||
# Signal to indicate that the info text about the connection has changed.
|
||||
connectionTextChanged = 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 = ""
|
||||
self._connection_text = ""
|
||||
|
||||
@pyqtProperty(str, notify = connectionTextChanged)
|
||||
def address(self):
|
||||
return self._address
|
||||
|
||||
def setConnectionText(self, connection_text):
|
||||
if self._connection_text != connection_text:
|
||||
self._connection_text = connection_text
|
||||
self.connectionTextChanged.emit()
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def connectionText(self):
|
||||
return self._connection_text
|
||||
|
||||
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 +183,4 @@ class ConnectionState(IntEnum):
|
|||
connecting = 1
|
||||
connected = 2
|
||||
busy = 3
|
||||
error = 4
|
||||
error = 4
|
||||
|
|
|
@ -136,6 +136,9 @@ class QualityManager:
|
|||
if basic_materials:
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
|
||||
empty_quality = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
|
||||
result.append(empty_quality)
|
||||
|
||||
return result
|
||||
|
||||
## Find all quality changes for a machine.
|
||||
|
|
|
@ -13,7 +13,7 @@ class BuildPlateDecorator(SceneNodeDecorator):
|
|||
# Make sure that groups are set correctly
|
||||
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
|
||||
self._build_plate_number = nr
|
||||
if issubclass(type(self._node), CuraSceneNode):
|
||||
if isinstance(self._node, CuraSceneNode):
|
||||
self._node.transformChanged() # trigger refresh node without introducing a new signal
|
||||
if self._node and self._node.callDecoration("isGroup"):
|
||||
for child in self._node.getChildren():
|
||||
|
|
|
@ -65,7 +65,7 @@ class ConvexHullNode(SceneNode):
|
|||
ConvexHullNode.shader.setUniformValue("u_opacity", 0.6)
|
||||
|
||||
if self.getParent():
|
||||
if self.getMeshData() and issubclass(type(self._node), SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate:
|
||||
if self.getMeshData() and isinstance(self._node, SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate:
|
||||
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
|
||||
if self._convex_hull_head_mesh:
|
||||
renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)
|
||||
|
|
|
@ -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__()
|
||||
|
||||
|
@ -30,7 +33,7 @@ class CuraSceneController(QObject):
|
|||
source = args[0]
|
||||
else:
|
||||
source = None
|
||||
if not issubclass(type(source), SceneNode):
|
||||
if not isinstance(source, SceneNode):
|
||||
return
|
||||
max_build_plate = self._calcMaxBuildPlate()
|
||||
changed = False
|
||||
|
@ -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():
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -270,7 +270,7 @@ class ExtruderManager(QObject):
|
|||
return []
|
||||
|
||||
# Get the extruders of all printable meshes in the scene
|
||||
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()]
|
||||
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()]
|
||||
for mesh in meshes:
|
||||
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
||||
if not extruder_stack_id:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -36,6 +36,8 @@ class ProfilesModel(InstanceContainersModel):
|
|||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
||||
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
||||
|
||||
self._empty_quality = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
|
||||
|
||||
# Factory function, used by QML
|
||||
@staticmethod
|
||||
def createProfilesModel(engine, js_engine):
|
||||
|
@ -85,13 +87,10 @@ class ProfilesModel(InstanceContainersModel):
|
|||
if quality.getMetaDataEntry("quality_type") not in quality_type_set:
|
||||
result.append(quality)
|
||||
|
||||
# if still profiles are found, add a single empty_quality ("Not supported") instance to the drop down list
|
||||
if len(result) == 0:
|
||||
# If not qualities are found we dynamically create a not supported container for this machine + material combination
|
||||
not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
|
||||
result.append(not_supported_container)
|
||||
if len(result) > 1:
|
||||
result.remove(self._empty_quality)
|
||||
|
||||
return {item.getId():item for item in result}, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
|
||||
return {item.getId(): item for item in result}, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
|
||||
|
||||
## Re-computes the items in this model, and adds the layer height role.
|
||||
def _recomputeItems(self):
|
||||
|
@ -114,7 +113,6 @@ class ProfilesModel(InstanceContainersModel):
|
|||
# active machine and material, and later yield the right ones.
|
||||
tmp_all_quality_items = OrderedDict()
|
||||
for item in super()._recomputeItems():
|
||||
|
||||
profiles = container_registry.findContainersMetadata(id = item["id"])
|
||||
if not profiles or "quality_type" not in profiles[0]:
|
||||
quality_type = ""
|
||||
|
|
|
@ -42,5 +42,7 @@ class QualityAndUserProfilesModel(ProfilesModel):
|
|||
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
|
||||
|
||||
result = filtered_quality_changes
|
||||
result.update({q.getId():q for q in quality_list})
|
||||
return result, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
|
||||
for q in quality_list:
|
||||
if q.getId() != "empty_quality":
|
||||
result[q.getId()] = q
|
||||
return result, {} #Only return true profiles for now, no metadata. The quality manager is not able to get only metadata yet.
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
@ -42,6 +40,8 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
|||
self.addRoleName(self.UserValueRole, "user_value")
|
||||
self.addRoleName(self.CategoryRole, "category")
|
||||
|
||||
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
||||
|
||||
def setExtruderId(self, extruder_id):
|
||||
if extruder_id != self._extruder_id:
|
||||
self._extruder_id = extruder_id
|
||||
|
@ -107,77 +107,87 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
|||
else:
|
||||
quality_changes_container = containers[0]
|
||||
|
||||
criteria = {
|
||||
"type": "quality",
|
||||
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
|
||||
"definition": quality_changes_container.getDefinition().getId()
|
||||
}
|
||||
if quality_changes_container.getMetaDataEntry("quality_type") == "not_supported":
|
||||
quality_container = self._empty_quality
|
||||
else:
|
||||
criteria = {
|
||||
"type": "quality",
|
||||
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
|
||||
"definition": quality_changes_container.getDefinition().getId()
|
||||
}
|
||||
|
||||
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
||||
if not quality_container:
|
||||
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
||||
return
|
||||
quality_container = quality_container[0]
|
||||
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
||||
if not quality_container:
|
||||
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
||||
return
|
||||
|
||||
quality_container = quality_container[0]
|
||||
|
||||
quality_type = quality_container.getMetaDataEntry("quality_type")
|
||||
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
|
||||
definition = quality_container.getDefinition()
|
||||
|
||||
# Check if the definition container has a translation file.
|
||||
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
|
||||
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
|
||||
if catalog.hasTranslationLoaded():
|
||||
self._i18n_catalog = catalog
|
||||
if quality_type == "not_supported":
|
||||
containers = []
|
||||
else:
|
||||
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
|
||||
definition = quality_container.getDefinition()
|
||||
|
||||
for file_name in quality_container.getDefinition().getInheritedFiles():
|
||||
catalog = i18nCatalog(os.path.basename(file_name))
|
||||
# Check if the definition container has a translation file.
|
||||
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
|
||||
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
|
||||
if catalog.hasTranslationLoaded():
|
||||
self._i18n_catalog = catalog
|
||||
|
||||
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
|
||||
for file_name in quality_container.getDefinition().getInheritedFiles():
|
||||
catalog = i18nCatalog(os.path.basename(file_name))
|
||||
if catalog.hasTranslationLoaded():
|
||||
self._i18n_catalog = catalog
|
||||
|
||||
if self._material_id and self._material_id != "empty_material":
|
||||
criteria["material"] = self._material_id
|
||||
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
|
||||
|
||||
criteria["extruder"] = self._extruder_id
|
||||
if self._material_id and self._material_id != "empty_material":
|
||||
criteria["material"] = self._material_id
|
||||
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
if not containers:
|
||||
# Try again, this time without extruder
|
||||
new_criteria = criteria.copy()
|
||||
new_criteria.pop("extruder")
|
||||
containers = self._container_registry.findInstanceContainers(**new_criteria)
|
||||
criteria["extruder"] = self._extruder_id
|
||||
|
||||
if not containers and "material" in criteria:
|
||||
# Try again, this time without material
|
||||
criteria.pop("material", None)
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
if not containers:
|
||||
# Try again, this time without extruder
|
||||
new_criteria = criteria.copy()
|
||||
new_criteria.pop("extruder")
|
||||
containers = self._container_registry.findInstanceContainers(**new_criteria)
|
||||
|
||||
if not containers:
|
||||
# Try again, this time without material or extruder
|
||||
criteria.pop("extruder") # "material" has already been popped
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
if not containers and "material" in criteria:
|
||||
# Try again, this time without material
|
||||
criteria.pop("material", None)
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
|
||||
if not containers:
|
||||
Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
|
||||
return
|
||||
if not containers:
|
||||
# Try again, this time without material or extruder
|
||||
criteria.pop("extruder") # "material" has already been popped
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
|
||||
if not containers:
|
||||
Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
|
||||
return
|
||||
|
||||
if quality_changes_container:
|
||||
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
|
||||
if self._extruder_definition_id != "":
|
||||
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
|
||||
if extruder_definitions:
|
||||
criteria["extruder"] = Application.getInstance().getMachineManager().getQualityDefinitionId(extruder_definitions[0])
|
||||
criteria["name"] = quality_changes_container.getName()
|
||||
if quality_type == "not_supported":
|
||||
criteria = {"type": "quality_changes", "quality_type": quality_type, "name": quality_changes_container.getName()}
|
||||
else:
|
||||
criteria["extruder"] = None
|
||||
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
|
||||
if self._extruder_definition_id != "":
|
||||
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
|
||||
if extruder_definitions:
|
||||
criteria["extruder"] = Application.getInstance().getMachineManager().getQualityDefinitionId(extruder_definitions[0])
|
||||
criteria["name"] = quality_changes_container.getName()
|
||||
else:
|
||||
criteria["extruder"] = None
|
||||
|
||||
changes = self._container_registry.findInstanceContainers(**criteria)
|
||||
if changes:
|
||||
containers.extend(changes)
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
|
||||
current_category = ""
|
||||
for definition in definition_container.findDefinitions():
|
||||
|
@ -213,15 +223,14 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
|||
if profile_value is None and user_value is None:
|
||||
continue
|
||||
|
||||
if is_multi_extrusion:
|
||||
settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
|
||||
# If a setting is not settable per extruder (global) and we're looking at an extruder tab, don't show this value.
|
||||
if self._extruder_id != "" and not settable_per_extruder:
|
||||
continue
|
||||
settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
|
||||
# If a setting is not settable per extruder (global) and we're looking at an extruder tab, don't show this value.
|
||||
if self._extruder_id != "" and not settable_per_extruder:
|
||||
continue
|
||||
|
||||
# If a setting is settable per extruder (not global) and we're looking at global tab, don't show this value.
|
||||
if self._extruder_id == "" and settable_per_extruder:
|
||||
continue
|
||||
# If a setting is settable per extruder (not global) and we're looking at global tab, don't show this value.
|
||||
if self._extruder_id == "" and settable_per_extruder:
|
||||
continue
|
||||
|
||||
label = definition.label
|
||||
if self._i18n_catalog:
|
||||
|
|
10
cura_app.py
10
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"):
|
||||
|
|
|
@ -609,7 +609,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
instance_container.setName(self._container_registry.uniqueName(instance_container.getName()))
|
||||
new_changes_container_id = self.getNewId(instance_container.getId())
|
||||
instance_container._id = new_changes_container_id
|
||||
instance_container.setMetaDataEntry("id", new_changes_container_id)
|
||||
|
||||
# TODO: we don't know the following is correct or not, need to verify
|
||||
# AND REFACTOR!!!
|
||||
|
|
|
@ -6,8 +6,9 @@ from UM.Math.Vector import Vector
|
|||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Application import Application
|
||||
import UM.Scene.SceneNode
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
import Savitar
|
||||
|
||||
|
@ -62,11 +63,15 @@ class ThreeMFWriter(MeshWriter):
|
|||
self._store_archive = store_archive
|
||||
|
||||
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||
# \returns Uranium Scenen node.
|
||||
# \returns Uranium Scene node.
|
||||
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
|
||||
if type(um_node) not in [UM.Scene.SceneNode.SceneNode, CuraSceneNode]:
|
||||
if not isinstance(um_node, SceneNode):
|
||||
return None
|
||||
|
||||
active_build_plate_nr = CuraApplication.getInstance().getBuildPlateModel().activeBuildPlate
|
||||
if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
||||
return
|
||||
|
||||
savitar_node = Savitar.SceneNode()
|
||||
|
||||
node_matrix = um_node.getLocalTransformation()
|
||||
|
@ -97,6 +102,9 @@ class ThreeMFWriter(MeshWriter):
|
|||
savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
|
||||
|
||||
for child_node in um_node.getChildren():
|
||||
# only save the nodes on the active build plate
|
||||
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
||||
continue
|
||||
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
|
||||
if savitar_child_node is not None:
|
||||
savitar_node.addChild(savitar_child_node)
|
||||
|
|
|
@ -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,23 +197,26 @@ 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
|
||||
|
||||
if self._process_layers_job:
|
||||
Logger.log("d", " ## Process layers job still busy, trying later")
|
||||
self._invokeSlice()
|
||||
return
|
||||
|
||||
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
|
||||
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
||||
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:
|
||||
Logger.log("d", "Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced)
|
||||
self._invokeSlice()
|
||||
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 = []
|
||||
|
@ -231,12 +233,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
|
||||
if not hasattr(self._scene, "gcode_list"):
|
||||
self._scene.gcode_list = {}
|
||||
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
|
||||
|
@ -408,7 +422,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
#
|
||||
# \param source The scene node that was changed.
|
||||
def _onSceneChanged(self, source):
|
||||
if not issubclass(type(source), SceneNode):
|
||||
if not isinstance(source, SceneNode):
|
||||
return
|
||||
|
||||
build_plate_changed = set()
|
||||
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
210
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal file
210
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal file
|
@ -0,0 +1,210 @@
|
|||
# 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 = None
|
||||
|
||||
if hasattr(scene, "gcode_dict"):
|
||||
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")
|
||||
|
||||
|
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal file
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal file
|
@ -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{ }
|
||||
}
|
||||
}
|
||||
}
|
2
plugins/PostProcessingPlugin/README.md
Normal file
2
plugins/PostProcessingPlugin/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# PostProcessingPlugin
|
||||
A post processing plugin for Cura
|
111
plugins/PostProcessingPlugin/Script.py
Normal file
111
plugins/PostProcessingPlugin/Script.py
Normal file
|
@ -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()
|
11
plugins/PostProcessingPlugin/__init__.py
Normal file
11
plugins/PostProcessingPlugin/__init__.py
Normal file
|
@ -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()}
|
8
plugins/PostProcessingPlugin/plugin.json
Normal file
8
plugins/PostProcessingPlugin/plugin.json
Normal file
|
@ -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"
|
||||
}
|
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal file
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="512px"
|
||||
height="512px"
|
||||
viewBox="0 0 512 512"
|
||||
style="enable-background:new 0 0 512 512;"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="postprocessing.svg"><metadata
|
||||
id="metadata9"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs7" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1104"
|
||||
inkscape:window-height="1006"
|
||||
id="namedview5"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.3359375"
|
||||
inkscape:cx="256"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-x="701"
|
||||
inkscape:window-y="121"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1" /><path
|
||||
d="M 402.15234 0 C 371.74552 4.7369516e-015 345.79114 10.752017 324.21875 32.324219 C 302.57788 53.89652 291.82617 79.851497 291.82617 110.18945 C 291.82617 127.34315 295.26662 143.09419 302.16602 157.44531 L 238.38477 221.20312 C 227.77569 210.95036 218.04331 201.50935 209.66016 193.32422 C 207.33386 190.99792 202.68042 189.48707 198.60938 191.92969 L 191.74609 196.11719 C 165.34252 169.24836 154.17609 158.42965 150.57031 145.40234 C 146.84822 131.79345 150.22148 113.64862 153.71094 106.90234 C 156.61882 101.55183 165.69233 96.550326 173.36914 95.96875 L 183.37109 106.20508 C 185.69739 108.53139 189.30456 108.53139 191.63086 106.20508 L 227.57227 69.681641 C 229.89858 67.355335 229.89858 63.517712 227.57227 61.191406 L 169.53125 2.21875 C 167.20494 -0.10755598 163.48147 -0.10755598 161.27148 2.21875 L 125.33008 38.742188 C 123.00378 41.068494 123.00378 44.906116 125.33008 47.232422 L 129.16992 51.1875 C 129.16992 56.88695 128.35573 65.727167 123.70312 70.496094 C 116.49157 77.823958 102.18413 69.332919 92.878906 75.962891 C 83.689998 82.476548 72.05746 92.944493 64.613281 100.38867 C 57.285417 107.83285 29.138171 137.37722 9.015625 187.16016 C -11.106922 236.94311 4.3632369 283.12 15.296875 295.2168 C 21.11264 301.61414 31.696982 308.12804 29.835938 296.03125 C 27.974892 283.81815 24.951448 241.47942 38.792969 224.14844 C 52.634489 206.81746 70.894726 192.62799 94.623047 191.46484 C 117.42084 190.30169 130.56529 198.09417 160.10938 228.10352 L 156.85156 234.15234 C 154.75788 238.10706 155.92175 243.10728 158.24805 245.43359 C 161.95717 248.74082 172.37305 258.96006 186.52539 273.04297 L 6.9511719 452.54883 C 2.2984329 457.14417 1.1842379e-015 462.71497 0 469.14844 C -1.1842379e-015 475.69681 2.2984329 481.15473 6.9511719 485.51953 L 26.308594 505.22266 C 31.018838 509.76054 36.589603 512 42.908203 512 C 49.341623 512 54.800053 509.76054 59.337891 505.22266 L 238.96875 325.6582 C 317.6609 404.95524 424.21289 513.40234 424.21289 513.40234 L 482.25391 454.43164 C 437.71428 411.9686 358.71135 336.76293 291.93164 272.71484 L 354.68945 209.98047 C 369.08663 216.91399 384.90203 220.37891 402.15234 220.37891 C 425.29988 220.37891 446.52947 213.53073 465.77344 199.83398 C 485.08493 186.1372 498.57775 168.33291 506.31641 146.34961 C 510.08303 135.39222 512 126.69334 512 120.25586 C 512 117.79044 511.24662 115.80572 509.87695 114.16211 C 508.50726 112.5185 506.59041 111.69531 504.125 111.69531 C 502.61835 111.69531 496.86414 114.5734 486.72852 120.39453 C 476.6614 126.21564 465.50054 132.85752 453.37891 140.32227 C 441.18878 147.78698 434.7515 151.75888 433.92969 152.23828 L 386.40234 125.94141 L 386.40234 70.8125 L 458.51562 29.242188 C 461.18649 27.461587 462.48633 25.202356 462.48633 22.394531 C 462.48633 19.586706 461.1865 17.325625 458.51562 15.476562 C 451.32484 10.545729 442.4896 6.780346 432.08008 4.0410156 C 421.60206 1.3701797 411.67159 0 402.15234 0 z "
|
||||
id="path3" /></svg>
|
After Width: | Height: | Size: 4.4 KiB |
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal file
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal file
|
@ -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
|
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal file
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal file
|
@ -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
|
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal file
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal file
|
@ -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
|
221
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal file
221
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal file
|
@ -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: <current_height> = <current_z> - <layer_0_z>
|
||||
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
|
169
plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py
Normal file
169
plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py
Normal file
|
@ -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
|
56
plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
Normal file
56
plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
Normal file
|
@ -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
|
469
plugins/PostProcessingPlugin/scripts/Stretch.py
Normal file
469
plugins/PostProcessingPlugin/scripts/Stretch.py
Normal file
|
@ -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)
|
||||
|
495
plugins/PostProcessingPlugin/scripts/TweakAtZ.py
Normal file
495
plugins/PostProcessingPlugin/scripts/TweakAtZ.py
Normal file
|
@ -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<factor in percent> - set speed factor override percentage
|
||||
## M221 S<factor in percent> - set flow factor override percentage
|
||||
## M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
|
||||
## M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
|
||||
## M140 S<temp> - set bed target temperature
|
||||
## M106 S<PWM> - set fan speed to target speed <S>
|
||||
## 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
|
|
@ -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 isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -145,35 +145,42 @@ geometry41core =
|
|||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
|
||||
//And reverse so that the line is also visible from the back side.
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
|
||||
|
||||
EndPrimitive();
|
||||
} else {
|
||||
// All normal lines are rendered as 3d tubes.
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
|
||||
|
||||
EndPrimitive();
|
||||
|
||||
// left side
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
|
||||
EndPrimitive();
|
||||
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
|
||||
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
|
||||
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
|
||||
|
||||
EndPrimitive();
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -10,13 +10,12 @@ Component
|
|||
{
|
||||
id: base
|
||||
property var manager: Cura.MachineManager.printerOutputDevices[0]
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("viewport_background")
|
||||
|
||||
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
|
||||
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
|
||||
|
||||
visible: manager != null
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("viewport_background")
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
|
@ -97,7 +96,7 @@ Component
|
|||
}
|
||||
Label
|
||||
{
|
||||
text: manager.numJobsPrinting
|
||||
text: manager.activePrintJobs.length
|
||||
font: UM.Theme.getFont("small")
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
@ -114,7 +113,7 @@ Component
|
|||
}
|
||||
Label
|
||||
{
|
||||
text: manager.numJobsQueued
|
||||
text: manager.queuedPrintJobs.length
|
||||
font: UM.Theme.getFont("small")
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@ Component
|
|||
width: maximumWidth
|
||||
height: maximumHeight
|
||||
color: UM.Theme.getColor("viewport_background")
|
||||
|
||||
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
|
||||
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
|
||||
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
|
@ -33,9 +33,9 @@ Component
|
|||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
text: OutputDevice.connectedPrinters.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
|
||||
text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
|
||||
|
||||
visible: OutputDevice.connectedPrinters.length == 0
|
||||
visible: OutputDevice.printers.length == 0
|
||||
}
|
||||
|
||||
Item
|
||||
|
@ -46,7 +46,7 @@ Component
|
|||
|
||||
width: Math.min(800 * screenScaleFactor, maximumWidth)
|
||||
height: children.height
|
||||
visible: OutputDevice.connectedPrinters.length != 0
|
||||
visible: OutputDevice.printers.length != 0
|
||||
|
||||
Label
|
||||
{
|
||||
|
@ -62,7 +62,6 @@ Component
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
ScrollView
|
||||
{
|
||||
id: printerScrollView
|
||||
|
@ -79,7 +78,7 @@ Component
|
|||
anchors.fill: parent
|
||||
spacing: -UM.Theme.getSize("default_lining").height
|
||||
|
||||
model: OutputDevice.connectedPrinters
|
||||
model: OutputDevice.printers
|
||||
|
||||
delegate: PrinterInfoBlock
|
||||
{
|
||||
|
@ -95,7 +94,7 @@ Component
|
|||
|
||||
PrinterVideoStream
|
||||
{
|
||||
visible: OutputDevice.selectedPrinterName != ""
|
||||
visible: OutputDevice.activePrinter != null
|
||||
anchors.fill:parent
|
||||
}
|
||||
}
|
||||
|
|
440
plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
Normal file
440
plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
Normal file
|
@ -0,0 +1,440 @@
|
|||
# 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.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the 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:
|
||||
if job.assignedPrinter:
|
||||
job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
|
||||
else:
|
||||
job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
|
||||
job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
|
||||
job_completed_message.show()
|
||||
|
||||
# Ensure UI gets updated
|
||||
self.printJobsChanged.emit()
|
||||
|
||||
# Keep a list of all completed jobs so we know if something changed next time.
|
||||
self._finished_jobs = finished_jobs
|
||||
|
||||
def _update(self):
|
||||
if not super()._update():
|
||||
return
|
||||
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
|
||||
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
|
||||
|
||||
def _onGetPrintJobsFinished(self, reply: QNetworkReply):
|
||||
if not checkValidGetReply(reply):
|
||||
return
|
||||
|
||||
result = loadJsonFromReply(reply)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
print_jobs_seen = []
|
||||
job_list_changed = False
|
||||
for print_job_data in result:
|
||||
print_job = findByKey(self._print_jobs, print_job_data["uuid"])
|
||||
|
||||
if print_job is None:
|
||||
print_job = self._createPrintJobModel(print_job_data)
|
||||
job_list_changed = True
|
||||
|
||||
self._updatePrintJob(print_job, print_job_data)
|
||||
|
||||
if print_job.state != "queued": # Print job should be assigned to a printer.
|
||||
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
|
||||
else: # The job can "reserve" a printer if some changes are required.
|
||||
printer = self._getPrinterByKey(print_job_data["assigned_to"])
|
||||
|
||||
if printer:
|
||||
printer.updateActivePrintJob(print_job)
|
||||
|
||||
print_jobs_seen.append(print_job)
|
||||
|
||||
# Check what jobs need to be removed.
|
||||
removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]
|
||||
|
||||
for removed_job in removed_jobs:
|
||||
job_list_changed |= self._removeJob(removed_job)
|
||||
|
||||
if job_list_changed:
|
||||
self.printJobsChanged.emit() # Do a single emit for all print job changes.
|
||||
|
||||
def _onGetPrintersDataFinished(self, reply: QNetworkReply):
|
||||
if not checkValidGetReply(reply):
|
||||
return
|
||||
|
||||
result = loadJsonFromReply(reply)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
printer_list_changed = False
|
||||
printers_seen = []
|
||||
|
||||
for printer_data in result:
|
||||
printer = findByKey(self._printers, printer_data["uuid"])
|
||||
|
||||
if printer is None:
|
||||
printer = self._createPrinterModel(printer_data)
|
||||
printer_list_changed = True
|
||||
|
||||
printers_seen.append(printer)
|
||||
|
||||
self._updatePrinter(printer, printer_data)
|
||||
|
||||
removed_printers = [printer for printer in self._printers if printer not in printers_seen]
|
||||
for printer in removed_printers:
|
||||
self._removePrinter(printer)
|
||||
|
||||
if removed_printers or printer_list_changed:
|
||||
self.printersChanged.emit()
|
||||
|
||||
def _createPrinterModel(self, data):
|
||||
printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
||||
number_of_extruders=self._number_of_extruders)
|
||||
printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
|
||||
self._printers.append(printer)
|
||||
return printer
|
||||
|
||||
def _createPrintJobModel(self, data):
|
||||
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
||||
key=data["uuid"], name= data["name"])
|
||||
print_job.stateChanged.connect(self._printJobStateChanged)
|
||||
self._print_jobs.append(print_job)
|
||||
return print_job
|
||||
|
||||
def _updatePrintJob(self, print_job, data):
|
||||
print_job.updateTimeTotal(data["time_total"])
|
||||
print_job.updateTimeElapsed(data["time_elapsed"])
|
||||
print_job.updateState(data["status"])
|
||||
print_job.updateOwner(data["owner"])
|
||||
|
||||
def _updatePrinter(self, printer, data):
|
||||
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
|
||||
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
|
||||
self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
|
||||
|
||||
printer.updateName(data["friendly_name"])
|
||||
printer.updateKey(data["uuid"])
|
||||
printer.updateType(data["machine_variant"])
|
||||
if not data["enabled"]:
|
||||
printer.updateState("disabled")
|
||||
else:
|
||||
printer.updateState(data["status"])
|
||||
|
||||
for index in range(0, self._number_of_extruders):
|
||||
extruder = printer.extruders[index]
|
||||
try:
|
||||
extruder_data = data["configuration"][index]
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
extruder.updateHotendID(extruder_data.get("print_core_id", ""))
|
||||
|
||||
material_data = extruder_data["material"]
|
||||
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
|
||||
GUID=material_data["guid"])
|
||||
if containers:
|
||||
color = containers[0].getMetaDataEntry("color_code")
|
||||
brand = containers[0].getMetaDataEntry("brand")
|
||||
material_type = containers[0].getMetaDataEntry("material")
|
||||
name = containers[0].getName()
|
||||
else:
|
||||
Logger.log("w",
|
||||
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
|
||||
guid=material_data["guid"]))
|
||||
color = material_data["color"]
|
||||
brand = material_data["brand"]
|
||||
material_type = material_data["material"]
|
||||
name = "Unknown"
|
||||
|
||||
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
|
||||
brand=brand, color=color, name=name)
|
||||
extruder.updateActiveMaterial(material)
|
||||
|
||||
def _removeJob(self, job):
|
||||
if job not in self._print_jobs:
|
||||
return False
|
||||
|
||||
if job.assignedPrinter:
|
||||
job.assignedPrinter.updateActivePrintJob(None)
|
||||
job.stateChanged.disconnect(self._printJobStateChanged)
|
||||
self._print_jobs.remove(job)
|
||||
|
||||
return True
|
||||
|
||||
def _removePrinter(self, printer):
|
||||
self._printers.remove(printer)
|
||||
if self._active_printer == printer:
|
||||
self._active_printer = None
|
||||
self.activePrinterChanged.emit()
|
||||
|
||||
|
||||
def loadJsonFromReply(reply):
|
||||
try:
|
||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.logException("w", "Unable to decode JSON from reply.")
|
||||
return
|
||||
return result
|
||||
|
||||
|
||||
def checkValidGetReply(reply):
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
|
||||
if status_code != 200:
|
||||
Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def findByKey(list, key):
|
||||
for item in list:
|
||||
if item.key == key:
|
||||
return item
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class ClusterUM3PrinterOutputController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
super().__init__(output_device)
|
||||
self.can_pre_heat_bed = False
|
||||
self.can_control_manually = False
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
data = "{\"action\": \"%s\"}" % state
|
||||
self._output_device.put("print_jobs/%s/action" % job.key, data, onFinished=None)
|
||||
|
|
@ -12,7 +12,10 @@ from cura.MachineAction import MachineAction
|
|||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class DiscoverUM3Action(MachineAction):
|
||||
discoveredDevicesChanged = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
|
||||
self._qml_url = "DiscoverUM3Action.qml"
|
||||
|
@ -25,24 +28,24 @@ class DiscoverUM3Action(MachineAction):
|
|||
|
||||
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
|
||||
|
||||
self._last_zeroconf_event_time = time.time()
|
||||
self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset
|
||||
self._last_zero_conf_event_time = time.time()
|
||||
|
||||
printersChanged = pyqtSignal()
|
||||
# Time to wait after a zero-conf service change before allowing a zeroconf reset
|
||||
self._zero_conf_change_grace_period = 0.25
|
||||
|
||||
@pyqtSlot()
|
||||
def startDiscovery(self):
|
||||
if not self._network_plugin:
|
||||
Logger.log("d", "Starting printer discovery.")
|
||||
Logger.log("d", "Starting device discovery.")
|
||||
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
|
||||
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
|
||||
self.printersChanged.emit()
|
||||
self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
## Re-filters the list of printers.
|
||||
## Re-filters the list of devices.
|
||||
@pyqtSlot()
|
||||
def reset(self):
|
||||
Logger.log("d", "Reset the list of found printers.")
|
||||
self.printersChanged.emit()
|
||||
Logger.log("d", "Reset the list of found devices.")
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def restartDiscovery(self):
|
||||
|
@ -51,43 +54,44 @@ class DiscoverUM3Action(MachineAction):
|
|||
# It's most likely that the QML engine is still creating delegates, where the python side already deleted or
|
||||
# garbage collected the data.
|
||||
# Whatever the case, waiting a bit ensures that it doesn't crash.
|
||||
if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period:
|
||||
if time.time() - self._last_zero_conf_event_time > self._zero_conf_change_grace_period:
|
||||
if not self._network_plugin:
|
||||
self.startDiscovery()
|
||||
else:
|
||||
self._network_plugin.startDiscovery()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def removeManualPrinter(self, key, address):
|
||||
def removeManualDevice(self, key, address):
|
||||
if not self._network_plugin:
|
||||
return
|
||||
|
||||
self._network_plugin.removeManualPrinter(key, address)
|
||||
self._network_plugin.removeManualDevice(key, address)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def setManualPrinter(self, key, address):
|
||||
def setManualDevice(self, key, address):
|
||||
if key != "":
|
||||
# This manual printer replaces a current manual printer
|
||||
self._network_plugin.removeManualPrinter(key)
|
||||
self._network_plugin.removeManualDevice(key)
|
||||
|
||||
if address != "":
|
||||
self._network_plugin.addManualPrinter(address)
|
||||
self._network_plugin.addManualDevice(address)
|
||||
|
||||
def _onPrinterDiscoveryChanged(self, *args):
|
||||
self._last_zeroconf_event_time = time.time()
|
||||
self.printersChanged.emit()
|
||||
def _onDeviceDiscoveryChanged(self, *args):
|
||||
self._last_zero_conf_event_time = time.time()
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = printersChanged)
|
||||
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
|
||||
def foundDevices(self):
|
||||
if self._network_plugin:
|
||||
# TODO: Check if this needs to stay.
|
||||
if Application.getInstance().getGlobalContainerStack():
|
||||
global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId()
|
||||
else:
|
||||
global_printer_type = "unknown"
|
||||
|
||||
printers = list(self._network_plugin.getPrinters().values())
|
||||
printers = list(self._network_plugin.getDiscoveredDevices().values())
|
||||
# TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet.
|
||||
printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
|
||||
#printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
|
||||
printers.sort(key = lambda k: k.name)
|
||||
return printers
|
||||
else:
|
||||
|
|
|
@ -10,7 +10,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: base
|
||||
anchors.fill: parent;
|
||||
property var selectedPrinter: null
|
||||
property var selectedDevice: null
|
||||
property bool completeProperties: true
|
||||
|
||||
Connections
|
||||
|
@ -29,9 +29,9 @@ Cura.MachineAction
|
|||
|
||||
function connectToPrinter()
|
||||
{
|
||||
if(base.selectedPrinter && base.completeProperties)
|
||||
if(base.selectedDevice && base.completeProperties)
|
||||
{
|
||||
var printerKey = base.selectedPrinter.getKey()
|
||||
var printerKey = base.selectedDevice.key
|
||||
if(manager.getStoredKey() != printerKey)
|
||||
{
|
||||
manager.setKey(printerKey);
|
||||
|
@ -83,10 +83,10 @@ Cura.MachineAction
|
|||
{
|
||||
id: editButton
|
||||
text: catalog.i18nc("@action:button", "Edit")
|
||||
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
|
||||
enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
|
||||
onClicked:
|
||||
{
|
||||
manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress);
|
||||
manualPrinterDialog.showDialog(base.selectedDevice.key, base.selectedDevice.ipAddress);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,8 +94,8 @@ Cura.MachineAction
|
|||
{
|
||||
id: removeButton
|
||||
text: catalog.i18nc("@action:button", "Remove")
|
||||
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
|
||||
onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress)
|
||||
enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
|
||||
onClicked: manager.removeManualDevice(base.selectedDevice.key, base.selectedDevice.ipAddress)
|
||||
}
|
||||
|
||||
Button
|
||||
|
@ -139,7 +139,7 @@ Cura.MachineAction
|
|||
{
|
||||
var selectedKey = manager.getStoredKey();
|
||||
for(var i = 0; i < model.length; i++) {
|
||||
if(model[i].getKey() == selectedKey)
|
||||
if(model[i].key == selectedKey)
|
||||
{
|
||||
currentIndex = i;
|
||||
return
|
||||
|
@ -151,9 +151,9 @@ Cura.MachineAction
|
|||
currentIndex: -1
|
||||
onCurrentIndexChanged:
|
||||
{
|
||||
base.selectedPrinter = listview.model[currentIndex];
|
||||
base.selectedDevice = listview.model[currentIndex];
|
||||
// Only allow connecting if the printer has responded to API query since the last refresh
|
||||
base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true";
|
||||
base.completeProperties = base.selectedDevice != null && base.selectedDevice.getProperty("incomplete") != "true";
|
||||
}
|
||||
Component.onCompleted: manager.startDiscovery()
|
||||
delegate: Rectangle
|
||||
|
@ -199,13 +199,13 @@ Cura.MachineAction
|
|||
Column
|
||||
{
|
||||
width: Math.floor(parent.width * 0.5)
|
||||
visible: base.selectedPrinter ? true : false
|
||||
visible: base.selectedDevice ? true : false
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
Label
|
||||
{
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: base.selectedPrinter ? base.selectedPrinter.name : ""
|
||||
text: base.selectedDevice ? base.selectedDevice.name : ""
|
||||
font: UM.Theme.getFont("large")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
@ -226,17 +226,17 @@ Cura.MachineAction
|
|||
wrapMode: Text.WordWrap
|
||||
text:
|
||||
{
|
||||
if(base.selectedPrinter)
|
||||
if(base.selectedDevice)
|
||||
{
|
||||
if(base.selectedPrinter.printerType == "ultimaker3")
|
||||
if(base.selectedDevice.printerType == "ultimaker3")
|
||||
{
|
||||
return catalog.i18nc("@label Printer name", "Ultimaker 3")
|
||||
} else if(base.selectedPrinter.printerType == "ultimaker3_extended")
|
||||
return catalog.i18nc("@label", "Ultimaker 3")
|
||||
} else if(base.selectedDevice.printerType == "ultimaker3_extended")
|
||||
{
|
||||
return catalog.i18nc("@label Printer name", "Ultimaker 3 Extended")
|
||||
return catalog.i18nc("@label", "Ultimaker 3 Extended")
|
||||
} else
|
||||
{
|
||||
return catalog.i18nc("@label Printer name", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
|
||||
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -255,7 +255,7 @@ Cura.MachineAction
|
|||
{
|
||||
width: Math.floor(parent.width * 0.5)
|
||||
wrapMode: Text.WordWrap
|
||||
text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : ""
|
||||
text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
|
||||
}
|
||||
Label
|
||||
{
|
||||
|
@ -267,7 +267,7 @@ Cura.MachineAction
|
|||
{
|
||||
width: Math.floor(parent.width * 0.5)
|
||||
wrapMode: Text.WordWrap
|
||||
text: base.selectedPrinter ? base.selectedPrinter.ipAddress : ""
|
||||
text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,17 +277,17 @@ Cura.MachineAction
|
|||
wrapMode: Text.WordWrap
|
||||
text:{
|
||||
// The property cluster size does not exist for older UM3 devices.
|
||||
if(!base.selectedPrinter || base.selectedPrinter.clusterSize == null || base.selectedPrinter.clusterSize == 1)
|
||||
if(!base.selectedDevice || base.selectedDevice.clusterSize == null || base.selectedDevice.clusterSize == 1)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else if (base.selectedPrinter.clusterSize === 0)
|
||||
else if (base.selectedDevice.clusterSize === 0)
|
||||
{
|
||||
return catalog.i18nc("@label", "This printer is not set up to host a group of Ultimaker 3 printers.");
|
||||
}
|
||||
else
|
||||
{
|
||||
return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedPrinter.clusterSize));
|
||||
return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedDevice.clusterSize));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -296,14 +296,14 @@ Cura.MachineAction
|
|||
{
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: base.selectedPrinter != null && !base.completeProperties
|
||||
visible: base.selectedDevice != null && !base.completeProperties
|
||||
text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
text: catalog.i18nc("@action:button", "Connect")
|
||||
enabled: (base.selectedPrinter && base.completeProperties) ? true : false
|
||||
enabled: (base.selectedDevice && base.completeProperties) ? true : false
|
||||
onClicked: connectToPrinter()
|
||||
}
|
||||
}
|
||||
|
@ -337,7 +337,7 @@ Cura.MachineAction
|
|||
|
||||
onAccepted:
|
||||
{
|
||||
manager.setManualPrinter(printerKey, addressText)
|
||||
manager.setManualDevice(printerKey, addressText)
|
||||
}
|
||||
|
||||
Column {
|
||||
|
|
638
plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
Normal file
638
plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
Normal file
|
@ -0,0 +1,638 @@
|
|||
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.
|
||||
self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated)
|
||||
|
||||
if self._authentication_state == AuthState.Authenticated:
|
||||
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network."))
|
||||
elif self._authentication_state == AuthState.AuthenticationRequested:
|
||||
self.setConnectionText(i18n_catalog.i18nc("@info:status",
|
||||
"Connected over the network. Please approve the access request on the printer."))
|
||||
elif self._authentication_state == AuthState.AuthenticationDenied:
|
||||
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer."))
|
||||
|
||||
|
||||
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
|
|
@ -0,0 +1,95 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from PyQt5.QtCore import QTimer
|
||||
from UM.Version import Version
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class LegacyUM3PrinterOutputController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
super().__init__(output_device)
|
||||
self._preheat_bed_timer = QTimer()
|
||||
self._preheat_bed_timer.setSingleShot(True)
|
||||
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
||||
self._preheat_printer = None
|
||||
|
||||
self.can_control_manually = False
|
||||
|
||||
# Are we still waiting for a response about preheat?
|
||||
# We need this so we can already update buttons, so it feels more snappy.
|
||||
self._preheat_request_in_progress = False
|
||||
|
||||
def isPreheatRequestInProgress(self):
|
||||
return self._preheat_request_in_progress
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
data = "{\"target\": \"%s\"}" % state
|
||||
self._output_device.put("print_job/state", data, onFinished=None)
|
||||
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
||||
data = str(temperature)
|
||||
self._output_device.put("printer/bed/temperature/target", data, onFinished=self._onPutBedTemperatureCompleted)
|
||||
|
||||
def _onPutBedTemperatureCompleted(self, reply):
|
||||
if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
|
||||
# If it was handling a preheat, it isn't anymore.
|
||||
self._preheat_request_in_progress = False
|
||||
|
||||
def _onPutPreheatBedCompleted(self, reply):
|
||||
self._preheat_request_in_progress = False
|
||||
|
||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
||||
head_pos = printer._head_position
|
||||
new_x = head_pos.x + x
|
||||
new_y = head_pos.y + y
|
||||
new_z = head_pos.z + z
|
||||
data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z)
|
||||
self._output_device.put("printer/heads/0/position", data, onFinished=None)
|
||||
|
||||
def homeBed(self, printer):
|
||||
self._output_device.put("printer/heads/0/position/z", "0", onFinished=None)
|
||||
|
||||
def _onPreheatBedTimerFinished(self):
|
||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
||||
self._preheat_printer.updateIsPreheating(False)
|
||||
self._preheat_request_in_progress = True
|
||||
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
||||
self.preheatBed(printer, temperature=0, duration=0)
|
||||
self._preheat_bed_timer.stop()
|
||||
printer.updateIsPreheating(False)
|
||||
|
||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
||||
try:
|
||||
temperature = round(temperature) # The API doesn't allow floating point.
|
||||
duration = round(duration)
|
||||
except ValueError:
|
||||
return # Got invalid values, can't pre-heat.
|
||||
|
||||
if duration > 0:
|
||||
data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration)
|
||||
else:
|
||||
data = """{"temperature": "%i"}""" % temperature
|
||||
|
||||
# Real bed pre-heating support is implemented from 3.5.92 and up.
|
||||
|
||||
if Version(printer.firmwareVersion) < Version("3.5.92"):
|
||||
# No firmware-side duration support then, so just set target bed temp and set a timer.
|
||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
||||
self._preheat_bed_timer.start()
|
||||
self._preheat_printer = printer
|
||||
printer.updateIsPreheating(True)
|
||||
return
|
||||
|
||||
self._output_device.put("printer/bed/pre_heat", data, onFinished = self._onPutPreheatBedCompleted)
|
||||
printer.updateIsPreheating(True)
|
||||
self._preheat_request_in_progress = True
|
||||
|
||||
|
|
@ -6,40 +6,49 @@ import Cura 1.0 as Cura
|
|||
|
||||
Component
|
||||
{
|
||||
Image
|
||||
Item
|
||||
{
|
||||
id: cameraImage
|
||||
property bool proportionalHeight:
|
||||
width: maximumWidth
|
||||
height: maximumHeight
|
||||
Image
|
||||
{
|
||||
if(sourceSize.height == 0 || maximumHeight == 0)
|
||||
id: cameraImage
|
||||
width: Math.min(sourceSize.width === 0 ? 800 * screenScaleFactor : sourceSize.width, maximumWidth)
|
||||
height: Math.floor((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
z: 1
|
||||
Component.onCompleted:
|
||||
{
|
||||
return true;
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.start()
|
||||
}
|
||||
}
|
||||
return (sourceSize.width / sourceSize.height) > (maximumWidth / maximumHeight);
|
||||
}
|
||||
property real _width: Math.floor(Math.min(maximumWidth, sourceSize.width))
|
||||
property real _height: Math.floor(Math.min(maximumHeight, sourceSize.height))
|
||||
width: proportionalHeight ? _width : Math.floor(sourceSize.width * _height / sourceSize.height)
|
||||
height: !proportionalHeight ? _height : Math.floor(sourceSize.height * _width / sourceSize.width)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
onVisibleChanged:
|
||||
{
|
||||
if(visible)
|
||||
onVisibleChanged:
|
||||
{
|
||||
OutputDevice.startCamera()
|
||||
} else
|
||||
{
|
||||
OutputDevice.stopCamera()
|
||||
if(visible)
|
||||
{
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.start()
|
||||
}
|
||||
} else
|
||||
{
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
source:
|
||||
{
|
||||
if(OutputDevice.cameraImage)
|
||||
source:
|
||||
{
|
||||
return OutputDevice.cameraImage;
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
|
||||
{
|
||||
return OutputDevice.activePrinter.camera.latestImage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,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 <filename>{file_name}</filename> to group {cluster_name}")
|
||||
file_name = os.path.basename(self._file_name).split(".")[0]
|
||||
self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1)
|
||||
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
|
||||
self._progress_message.actionTriggered.connect(self._onMessageActionTriggered)
|
||||
self._progress_message.show()
|
||||
|
||||
def _addUserAgentHeader(self, request):
|
||||
request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin")
|
||||
|
||||
def _cleanupRequest(self):
|
||||
self._request = None
|
||||
self._stage = OutputStage.ready
|
||||
self._file_name = None
|
||||
|
||||
def _onFinished(self, reply):
|
||||
super()._onFinished(reply)
|
||||
reply_url = reply.url().toString()
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status_code == 500:
|
||||
Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url))
|
||||
return
|
||||
if reply.error() == QNetworkReply.ContentOperationNotPermittedError:
|
||||
# It was probably "/api/v1/materials" for legacy UM3
|
||||
return
|
||||
if reply.error() == QNetworkReply.ContentNotFoundError:
|
||||
# It was probably "/api/v1/print_job" for legacy UM3
|
||||
return
|
||||
|
||||
if reply.operation() == QNetworkAccessManager.PostOperation:
|
||||
if self._cluster_api_prefix + "print_jobs" in reply_url:
|
||||
self._finishedPrintJobPostRequest(reply)
|
||||
return
|
||||
|
||||
# We need to do this check *after* we process the post operation!
|
||||
# If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this.
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error())
|
||||
return
|
||||
|
||||
elif reply.operation() == QNetworkAccessManager.GetOperation:
|
||||
if self._cluster_api_prefix + "print_jobs" in reply_url:
|
||||
self._finishedPrintJobsRequest(reply)
|
||||
elif self._cluster_api_prefix + "printers" in reply_url:
|
||||
self._finishedPrintersRequest(reply)
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrintJobControlPanel(self):
|
||||
Logger.log("d", "Opening print job control panel...")
|
||||
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrinterControlPanel(self):
|
||||
Logger.log("d", "Opening printer control panel...")
|
||||
QDesktopServices.openUrl(QUrl(self.getPrintersUrl()))
|
||||
|
||||
def _onMessageActionTriggered(self, message, action):
|
||||
if action == "open_browser":
|
||||
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
|
||||
|
||||
if action == "Abort":
|
||||
Logger.log("d", "User aborted sending print to remote.")
|
||||
self._progress_message.hide()
|
||||
self._compressing_print = False
|
||||
if self._reply:
|
||||
self._reply.abort()
|
||||
self._stage = OutputStage.ready
|
||||
Application.getInstance().getController().setActiveStage("PrepareStage")
|
||||
|
||||
@pyqtSlot(int, result=str)
|
||||
def formatDuration(self, seconds):
|
||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
||||
|
||||
## For cluster below
|
||||
def _get_plugin_directory_name(self):
|
||||
current_file_absolute_path = os.path.realpath(__file__)
|
||||
directory_path = os.path.dirname(current_file_absolute_path)
|
||||
_, directory_name = os.path.split(directory_path)
|
||||
return directory_name
|
||||
|
||||
@property
|
||||
def _plugin_path(self):
|
||||
return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name())
|
File diff suppressed because it is too large
Load diff
|
@ -1,361 +0,0 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import time
|
||||
import json
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo # type: ignore
|
||||
|
||||
from . import NetworkPrinterOutputDevice, NetworkClusterPrinterOutputDevice
|
||||
|
||||
|
||||
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
|
||||
# Zero-Conf is used to detect printers, which are saved in a dict.
|
||||
# If we discover a printer that has the same key as the active machine instance a connection is made.
|
||||
@signalemitter
|
||||
class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._zero_conf = None
|
||||
self._browser = None
|
||||
self._printers = {}
|
||||
self._cluster_printers_seen = {} # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer
|
||||
|
||||
self._api_version = "1"
|
||||
self._api_prefix = "/api/v" + self._api_version + "/"
|
||||
self._cluster_api_version = "1"
|
||||
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
|
||||
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
self._network_manager.finished.connect(self._onNetworkRequestFinished)
|
||||
|
||||
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
|
||||
# authentication requests.
|
||||
self._old_printers = []
|
||||
self._excluded_addresses = ["127.0.0.1"] # Adding a list of not allowed IP addresses. At this moment, just localhost
|
||||
|
||||
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
self.addPrinterSignal.connect(self.addPrinter)
|
||||
self.removePrinterSignal.connect(self.removePrinter)
|
||||
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
|
||||
|
||||
# Get list of manual printers from preferences
|
||||
self._preferences = Preferences.getInstance()
|
||||
self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames
|
||||
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
|
||||
|
||||
self._network_requests_buffer = {} # store api responses until data is complete
|
||||
|
||||
# The zeroconf service changed requests are handled in a separate thread, so we can re-schedule the requests
|
||||
# which fail to get detailed service info.
|
||||
# Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
|
||||
# them up and process them.
|
||||
self._service_changed_request_queue = Queue()
|
||||
self._service_changed_request_event = Event()
|
||||
self._service_changed_request_thread = Thread(target = self._handleOnServiceChangedRequests,
|
||||
daemon = True)
|
||||
self._service_changed_request_thread.start()
|
||||
|
||||
addPrinterSignal = Signal()
|
||||
removePrinterSignal = Signal()
|
||||
printerListChanged = Signal()
|
||||
|
||||
## Start looking for devices on network.
|
||||
def start(self):
|
||||
self.startDiscovery()
|
||||
|
||||
def startDiscovery(self):
|
||||
self.stop()
|
||||
if self._browser:
|
||||
self._browser.cancel()
|
||||
self._browser = None
|
||||
self._old_printers = [printer_name for printer_name in self._printers]
|
||||
self._printers = {}
|
||||
self.printerListChanged.emit()
|
||||
# After network switching, one must make a new instance of Zeroconf
|
||||
# On windows, the instance creation is very fast (unnoticable). Other platforms?
|
||||
self._zero_conf = Zeroconf()
|
||||
self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest])
|
||||
|
||||
# Look for manual instances from preference
|
||||
for address in self._manual_instances:
|
||||
if address:
|
||||
self.addManualPrinter(address)
|
||||
|
||||
def addManualPrinter(self, address):
|
||||
if address not in self._manual_instances:
|
||||
self._manual_instances.append(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
instance_name = "manual:%s" % address
|
||||
properties = {
|
||||
b"name": address.encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"incomplete": b"true"
|
||||
}
|
||||
|
||||
if instance_name not in self._printers:
|
||||
# Add a preliminary printer instance
|
||||
self.addPrinter(instance_name, address, properties)
|
||||
|
||||
self.checkManualPrinter(address)
|
||||
self.checkClusterPrinter(address)
|
||||
|
||||
def removeManualPrinter(self, key, address = None):
|
||||
if key in self._printers:
|
||||
if not address:
|
||||
address = self._printers[key].ipAddress
|
||||
self.removePrinter(key)
|
||||
|
||||
if address in self._manual_instances:
|
||||
self._manual_instances.remove(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
def checkManualPrinter(self, address):
|
||||
# Check if a printer exists at this address
|
||||
# If a printer responds, it will replace the preliminary printer created above
|
||||
# origin=manual is for tracking back the origin of the call
|
||||
url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name")
|
||||
name_request = QNetworkRequest(url)
|
||||
self._network_manager.get(name_request)
|
||||
|
||||
def checkClusterPrinter(self, address):
|
||||
cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster")
|
||||
cluster_request = QNetworkRequest(cluster_url)
|
||||
self._network_manager.get(cluster_request)
|
||||
|
||||
## Handler for all requests that have finished.
|
||||
def _onNetworkRequestFinished(self, reply):
|
||||
reply_url = reply.url().toString()
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
|
||||
if reply.operation() == QNetworkAccessManager.GetOperation:
|
||||
address = reply.url().host()
|
||||
if "origin=manual_name" in reply_url: # Name returned from printer.
|
||||
if status_code == 200:
|
||||
|
||||
try:
|
||||
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
Logger.log("e", "Printer returned invalid JSON.")
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
Logger.log("e", "Printer returned incorrect UTF-8.")
|
||||
return
|
||||
|
||||
if address not in self._network_requests_buffer:
|
||||
self._network_requests_buffer[address] = {}
|
||||
self._network_requests_buffer[address]["system"] = system_info
|
||||
elif "origin=check_cluster" in reply_url:
|
||||
if address not in self._network_requests_buffer:
|
||||
self._network_requests_buffer[address] = {}
|
||||
if status_code == 200:
|
||||
# We know it's a cluster printer
|
||||
Logger.log("d", "Cluster printer detected: [%s]", reply.url())
|
||||
|
||||
try:
|
||||
cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
Logger.log("e", "Printer returned invalid JSON.")
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
Logger.log("e", "Printer returned incorrect UTF-8.")
|
||||
return
|
||||
|
||||
self._network_requests_buffer[address]["cluster"] = True
|
||||
self._network_requests_buffer[address]["cluster_size"] = len(cluster_printers_list)
|
||||
else:
|
||||
Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url())
|
||||
self._network_requests_buffer[address]["cluster"] = False
|
||||
|
||||
# Both the system call and cluster call are finished
|
||||
if (address in self._network_requests_buffer and
|
||||
"system" in self._network_requests_buffer[address] and
|
||||
"cluster" in self._network_requests_buffer[address]):
|
||||
|
||||
instance_name = "manual:%s" % address
|
||||
system_info = self._network_requests_buffer[address]["system"]
|
||||
machine = "unknown"
|
||||
if "variant" in system_info:
|
||||
variant = system_info["variant"]
|
||||
if variant == "Ultimaker 3":
|
||||
machine = "9066"
|
||||
elif variant == "Ultimaker 3 Extended":
|
||||
machine = "9511"
|
||||
|
||||
properties = {
|
||||
b"name": system_info["name"].encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"firmware_version": system_info["firmware"].encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"machine": machine.encode("utf-8")
|
||||
}
|
||||
|
||||
if self._network_requests_buffer[address]["cluster"]:
|
||||
properties[b"cluster_size"] = self._network_requests_buffer[address]["cluster_size"]
|
||||
|
||||
if instance_name in self._printers:
|
||||
# Only replace the printer if it is still in the list of (manual) printers
|
||||
self.removePrinter(instance_name)
|
||||
self.addPrinter(instance_name, address, properties)
|
||||
|
||||
del self._network_requests_buffer[address]
|
||||
|
||||
## Stop looking for devices on network.
|
||||
def stop(self):
|
||||
if self._zero_conf is not None:
|
||||
Logger.log("d", "zeroconf close...")
|
||||
self._zero_conf.close()
|
||||
|
||||
def getPrinters(self):
|
||||
return self._printers
|
||||
|
||||
def reCheckConnections(self):
|
||||
active_machine = Application.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
||||
for key in self._printers:
|
||||
if key == active_machine.getMetaDataEntry("um_network_key"):
|
||||
if not self._printers[key].isConnected():
|
||||
Logger.log("d", "Connecting [%s]..." % key)
|
||||
self._printers[key].connect()
|
||||
self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
|
||||
else:
|
||||
if self._printers[key].isConnected():
|
||||
Logger.log("d", "Closing connection [%s]..." % key)
|
||||
self._printers[key].close()
|
||||
self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
|
||||
|
||||
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
def addPrinter(self, name, address, properties):
|
||||
cluster_size = int(properties.get(b"cluster_size", -1))
|
||||
if cluster_size >= 0:
|
||||
printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice(
|
||||
name, address, properties, self._api_prefix)
|
||||
else:
|
||||
printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
|
||||
self._printers[printer.getKey()] = printer
|
||||
self._cluster_printers_seen[printer.getKey()] = name # Cluster printers that may be temporary unreachable or is rebooted keep being stored here
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
|
||||
if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced?
|
||||
Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey())
|
||||
self._printers[printer.getKey()].connect()
|
||||
printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
|
||||
self.printerListChanged.emit()
|
||||
|
||||
def removePrinter(self, name):
|
||||
printer = self._printers.pop(name, None)
|
||||
if printer:
|
||||
if printer.isConnected():
|
||||
printer.disconnect()
|
||||
printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
|
||||
Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
|
||||
self.printerListChanged.emit()
|
||||
|
||||
## Handler for when the connection state of one of the detected printers changes
|
||||
def _onPrinterConnectionStateChanged(self, key):
|
||||
if key not in self._printers:
|
||||
return
|
||||
if self._printers[key].isConnected():
|
||||
self.getOutputDeviceManager().addOutputDevice(self._printers[key])
|
||||
else:
|
||||
self.getOutputDeviceManager().removeOutputDevice(key)
|
||||
|
||||
## Handler for zeroConf detection.
|
||||
# Return True or False indicating if the process succeeded.
|
||||
def _onServiceChanged(self, zeroconf, service_type, name, state_change):
|
||||
if state_change == ServiceStateChange.Added:
|
||||
Logger.log("d", "Bonjour service added: %s" % name)
|
||||
|
||||
# First try getting info from zeroconf cache
|
||||
info = ServiceInfo(service_type, name, properties = {})
|
||||
for record in zeroconf.cache.entries_with_name(name.lower()):
|
||||
info.update_record(zeroconf, time.time(), record)
|
||||
|
||||
for record in zeroconf.cache.entries_with_name(info.server):
|
||||
info.update_record(zeroconf, time.time(), record)
|
||||
if info.address:
|
||||
break
|
||||
|
||||
# Request more data if info is not complete
|
||||
if not info.address:
|
||||
Logger.log("d", "Trying to get address of %s", name)
|
||||
info = zeroconf.get_service_info(service_type, name)
|
||||
|
||||
if info:
|
||||
type_of_device = info.properties.get(b"type", None)
|
||||
if type_of_device:
|
||||
if type_of_device == b"printer":
|
||||
address = '.'.join(map(lambda n: str(n), info.address))
|
||||
if address in self._excluded_addresses:
|
||||
Logger.log("d", "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.", address, name)
|
||||
return False # When getting the localhost IP, then try to reconnect
|
||||
self.addPrinterSignal.emit(str(name), address, info.properties)
|
||||
else:
|
||||
Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device )
|
||||
else:
|
||||
Logger.log("w", "Could not get information about %s" % name)
|
||||
return False
|
||||
|
||||
elif state_change == ServiceStateChange.Removed:
|
||||
Logger.log("d", "Bonjour service removed: %s" % name)
|
||||
self.removePrinterSignal.emit(str(name))
|
||||
|
||||
return True
|
||||
|
||||
## Appends a service changed request so later the handling thread will pick it up and processes it.
|
||||
def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
|
||||
# append the request and set the event so the event handling thread can pick it up
|
||||
item = (zeroconf, service_type, name, state_change)
|
||||
self._service_changed_request_queue.put(item)
|
||||
self._service_changed_request_event.set()
|
||||
|
||||
def _handleOnServiceChangedRequests(self):
|
||||
while True:
|
||||
# wait for the event to be set
|
||||
self._service_changed_request_event.wait(timeout = 5.0)
|
||||
# stop if the application is shutting down
|
||||
if Application.getInstance().isShuttingDown():
|
||||
return
|
||||
|
||||
self._service_changed_request_event.clear()
|
||||
|
||||
# handle all pending requests
|
||||
reschedule_requests = [] # a list of requests that have failed so later they will get re-scheduled
|
||||
while not self._service_changed_request_queue.empty():
|
||||
request = self._service_changed_request_queue.get()
|
||||
zeroconf, service_type, name, state_change = request
|
||||
try:
|
||||
result = self._onServiceChanged(zeroconf, service_type, name, state_change)
|
||||
if not result:
|
||||
reschedule_requests.append(request)
|
||||
except Exception:
|
||||
Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
|
||||
service_type, name)
|
||||
reschedule_requests.append(request)
|
||||
|
||||
# re-schedule the failed requests if any
|
||||
if reschedule_requests:
|
||||
for request in reschedule_requests:
|
||||
self._service_changed_request_queue.put(request)
|
||||
|
||||
@pyqtSlot()
|
||||
def openControlPanel(self):
|
||||
Logger.log("d", "Opening print jobs web UI...")
|
||||
selected_device = self.getOutputDeviceManager().getActiveDevice()
|
||||
if isinstance(selected_device, NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice):
|
||||
QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl()))
|
|
@ -15,7 +15,7 @@ Item
|
|||
Label
|
||||
{
|
||||
id: materialLabel
|
||||
text: printCoreConfiguration.material.material + " (" + printCoreConfiguration.material.color + ")"
|
||||
text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : ""
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("very_small")
|
||||
|
@ -23,7 +23,7 @@ Item
|
|||
Label
|
||||
{
|
||||
id: printCoreLabel
|
||||
text: printCoreConfiguration.print_core_id
|
||||
text: printCoreConfiguration.hotendID
|
||||
anchors.top: materialLabel.bottom
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
|
|
|
@ -20,8 +20,24 @@ UM.Dialog
|
|||
|
||||
visible: true
|
||||
modality: Qt.ApplicationModal
|
||||
onVisibleChanged:
|
||||
{
|
||||
if(visible)
|
||||
{
|
||||
resetPrintersModel()
|
||||
}
|
||||
}
|
||||
title: catalog.i18nc("@title:window", "Print over network")
|
||||
|
||||
title: catalog.i18nc("@title:window","Print over network")
|
||||
property var printersModel: ListModel{}
|
||||
function resetPrintersModel() {
|
||||
printersModel.append({ name: "Automatic", key: ""})
|
||||
|
||||
for (var index in OutputDevice.printers)
|
||||
{
|
||||
printersModel.append({name: OutputDevice.printers[index].name, key: OutputDevice.printers[index].key})
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
|
@ -31,8 +47,7 @@ UM.Dialog
|
|||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
height: 50 * screenScaleFactor
|
||||
|
||||
height: 50 * screenScaleFactord
|
||||
Label
|
||||
{
|
||||
id: manualPrinterSelectionLabel
|
||||
|
@ -42,7 +57,7 @@ UM.Dialog
|
|||
topMargin: UM.Theme.getSize("default_margin").height
|
||||
right: parent.right
|
||||
}
|
||||
text: "Printer selection"
|
||||
text: catalog.i18nc("@label", "Printer selection")
|
||||
wrapMode: Text.Wrap
|
||||
height: 20 * screenScaleFactor
|
||||
}
|
||||
|
@ -50,18 +65,12 @@ UM.Dialog
|
|||
ComboBox
|
||||
{
|
||||
id: printerSelectionCombobox
|
||||
model: OutputDevice.printers
|
||||
textRole: "friendly_name"
|
||||
model: base.printersModel
|
||||
textRole: "name"
|
||||
|
||||
width: parent.width
|
||||
height: 40 * screenScaleFactor
|
||||
Behavior on height { NumberAnimation { duration: 100 } }
|
||||
|
||||
onActivated:
|
||||
{
|
||||
var printerData = OutputDevice.printers[index];
|
||||
OutputDevice.selectPrinter(printerData.unique_name, printerData.friendly_name);
|
||||
}
|
||||
}
|
||||
|
||||
SystemPalette
|
||||
|
@ -79,8 +88,6 @@ UM.Dialog
|
|||
enabled: true
|
||||
onClicked: {
|
||||
base.visible = false;
|
||||
// reset to defaults
|
||||
OutputDevice.selectAutomaticPrinter()
|
||||
printerSelectionCombobox.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
@ -93,9 +100,8 @@ UM.Dialog
|
|||
enabled: true
|
||||
onClicked: {
|
||||
base.visible = false;
|
||||
OutputDevice.sendPrintJob();
|
||||
OutputDevice.sendPrintJob(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
|
||||
// reset to defaults
|
||||
OutputDevice.selectAutomaticPrinter()
|
||||
printerSelectionCombobox.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,16 +22,16 @@ Rectangle
|
|||
{
|
||||
return "";
|
||||
}
|
||||
if (printJob.time_total === 0)
|
||||
if (printJob.timeTotal === 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return Math.min(100, Math.round(printJob.time_elapsed / printJob.time_total * 100)) + "%";
|
||||
return Math.min(100, Math.round(printJob.timeElapsed / printJob.timeTotal * 100)) + "%";
|
||||
}
|
||||
|
||||
function printerStatusText(printer)
|
||||
{
|
||||
switch (printer.status)
|
||||
switch (printer.state)
|
||||
{
|
||||
case "pre_print":
|
||||
return catalog.i18nc("@label", "Preparing to print")
|
||||
|
@ -49,31 +49,23 @@ Rectangle
|
|||
}
|
||||
|
||||
id: printerDelegate
|
||||
property var printer
|
||||
|
||||
property var printer: null
|
||||
property var printJob: printer != null ? printer.activePrintJob: null
|
||||
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: mouse.containsMouse ? emphasisColor : lineColor
|
||||
z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible.
|
||||
|
||||
property var printJob:
|
||||
{
|
||||
if (printer.reserved_by != null)
|
||||
{
|
||||
// Look in another list.
|
||||
return OutputDevice.printJobsByUUID[printer.reserved_by]
|
||||
}
|
||||
return OutputDevice.printJobsByPrinterUUID[printer.uuid]
|
||||
}
|
||||
|
||||
MouseArea
|
||||
{
|
||||
id: mouse
|
||||
anchors.fill:parent
|
||||
onClicked: OutputDevice.selectPrinter(printer.unique_name, printer.friendly_name)
|
||||
onClicked: OutputDevice.setActivePrinter(printer)
|
||||
hoverEnabled: true;
|
||||
|
||||
// Only clickable if no printer is selected
|
||||
enabled: OutputDevice.selectedPrinterName == "" && printer.status !== "unreachable"
|
||||
enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
|
||||
}
|
||||
|
||||
Row
|
||||
|
@ -122,7 +114,7 @@ Rectangle
|
|||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
text: printJob != null ? getPrettyTime(printJob.time_total) : ""
|
||||
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
|
||||
opacity: 0.65
|
||||
font: UM.Theme.getFont("default")
|
||||
elide: Text.ElideRight
|
||||
|
@ -140,7 +132,7 @@ Rectangle
|
|||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
|
||||
text: printer.friendly_name
|
||||
text: printer.name
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
@ -150,7 +142,7 @@ Rectangle
|
|||
id: printerTypeLabel
|
||||
anchors.top: printerNameLabel.bottom
|
||||
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
|
||||
text: printer.machine_variant
|
||||
text: printer.type
|
||||
anchors.left: parent.left
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("very_small")
|
||||
|
@ -166,7 +158,7 @@ Rectangle
|
|||
anchors.right: printProgressArea.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
color: emphasisColor
|
||||
opacity: printer != null && printer.status === "unreachable" ? 0.3 : 1
|
||||
opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
|
||||
|
||||
Image
|
||||
{
|
||||
|
@ -192,7 +184,7 @@ Rectangle
|
|||
{
|
||||
id: leftExtruderInfo
|
||||
width: Math.floor((parent.width - extruderSeperator.width) / 2)
|
||||
printCoreConfiguration: printer.configuration[0]
|
||||
printCoreConfiguration: printer.extruders[0]
|
||||
}
|
||||
|
||||
Rectangle
|
||||
|
@ -207,7 +199,7 @@ Rectangle
|
|||
{
|
||||
id: rightExtruderInfo
|
||||
width: Math.floor((parent.width - extruderSeperator.width) / 2)
|
||||
printCoreConfiguration: printer.configuration[1]
|
||||
printCoreConfiguration: printer.extruders[1]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,9 +217,9 @@ Rectangle
|
|||
if(printJob != null)
|
||||
{
|
||||
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"];
|
||||
return extendStates.indexOf(printJob.status) !== -1;
|
||||
return extendStates.indexOf(printJob.state) !== -1;
|
||||
}
|
||||
return !printer.enabled;
|
||||
return printer.state == "disabled"
|
||||
}
|
||||
|
||||
Item // Status and Percent
|
||||
|
@ -235,7 +227,7 @@ Rectangle
|
|||
id: printProgressTitleBar
|
||||
|
||||
property var showPercent: {
|
||||
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.status) !== -1);
|
||||
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.state) !== -1);
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
|
@ -252,19 +244,19 @@ Rectangle
|
|||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (!printer.enabled)
|
||||
if (printer.state == "disabled")
|
||||
{
|
||||
return catalog.i18nc("@label:status", "Disabled");
|
||||
}
|
||||
|
||||
if (printer.status === "unreachable")
|
||||
if (printer.state === "unreachable")
|
||||
{
|
||||
return printerStatusText(printer);
|
||||
}
|
||||
|
||||
if (printJob != null)
|
||||
{
|
||||
switch (printJob.status)
|
||||
switch (printJob.state)
|
||||
{
|
||||
case "printing":
|
||||
case "post_print":
|
||||
|
@ -277,14 +269,7 @@ Rectangle
|
|||
case "sent_to_printer":
|
||||
return catalog.i18nc("@label", "Preparing to print")
|
||||
case "queued":
|
||||
if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
|
||||
{
|
||||
return catalog.i18nc("@label:status", "Action required");
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
case "pausing":
|
||||
case "paused":
|
||||
return catalog.i18nc("@label:status", "Paused");
|
||||
|
@ -328,26 +313,23 @@ Rectangle
|
|||
visible: !printProgressTitleBar.showPercent
|
||||
|
||||
source: {
|
||||
if (!printer.enabled)
|
||||
if (printer.state == "disabled")
|
||||
{
|
||||
return "blocked-icon.svg";
|
||||
}
|
||||
|
||||
if (printer.status === "unreachable")
|
||||
if (printer.state === "unreachable")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (printJob != null)
|
||||
{
|
||||
if(printJob.status === "queued")
|
||||
if(printJob.state === "queued")
|
||||
{
|
||||
if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
|
||||
{
|
||||
return "action-required-icon.svg";
|
||||
}
|
||||
return "action-required-icon.svg";
|
||||
}
|
||||
else if (printJob.status === "wait_cleanup")
|
||||
else if (printJob.state === "wait_cleanup")
|
||||
{
|
||||
return "checkmark-icon.svg";
|
||||
}
|
||||
|
@ -384,23 +366,23 @@ Rectangle
|
|||
{
|
||||
text:
|
||||
{
|
||||
if (!printer.enabled)
|
||||
if (printer.state == "disabled")
|
||||
{
|
||||
return catalog.i18nc("@label", "Not accepting print jobs");
|
||||
}
|
||||
|
||||
if (printer.status === "unreachable")
|
||||
if (printer.state === "unreachable")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if(printJob != null)
|
||||
{
|
||||
switch (printJob.status)
|
||||
switch (printJob.state)
|
||||
{
|
||||
case "printing":
|
||||
case "post_print":
|
||||
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.time_total - printJob.time_elapsed)
|
||||
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.timeTotal - printJob.timeElapsed)
|
||||
case "wait_cleanup":
|
||||
return catalog.i18nc("@label", "Clear build plate")
|
||||
case "sent_to_printer":
|
||||
|
@ -409,10 +391,7 @@ Rectangle
|
|||
case "wait_for_configuration":
|
||||
return catalog.i18nc("@label", "Not accepting print jobs")
|
||||
case "queued":
|
||||
if (printJob.configuration_changes_required != undefined)
|
||||
{
|
||||
return catalog.i18nc("@label", "Waiting for configuration change");
|
||||
}
|
||||
return catalog.i18nc("@label", "Waiting for configuration change");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -432,7 +411,7 @@ Rectangle
|
|||
text: {
|
||||
if(printJob != null)
|
||||
{
|
||||
if(printJob.status == "printing" || printJob.status == "post_print")
|
||||
if(printJob.state == "printing" || printJob.state == "post_print")
|
||||
{
|
||||
return OutputDevice.getDateCompleted(printJob.time_total - printJob.time_elapsed)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ Item
|
|||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
onClicked: OutputDevice.selectAutomaticPrinter()
|
||||
onClicked: OutputDevice.setActivePrinter(null)
|
||||
z: 0
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ Item
|
|||
width: 20 * screenScaleFactor
|
||||
height: 20 * screenScaleFactor
|
||||
|
||||
onClicked: OutputDevice.selectAutomaticPrinter()
|
||||
onClicked: OutputDevice.setActivePrinter(null)
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
|
@ -65,17 +65,23 @@ Item
|
|||
{
|
||||
if(visible)
|
||||
{
|
||||
OutputDevice.startCamera()
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.start()
|
||||
}
|
||||
} else
|
||||
{
|
||||
OutputDevice.stopCamera()
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
source:
|
||||
{
|
||||
if(OutputDevice.cameraImage)
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
|
||||
{
|
||||
return OutputDevice.cameraImage;
|
||||
return OutputDevice.activePrinter.camera.latestImage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ Item
|
|||
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
|
||||
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
|
||||
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
||||
property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested
|
||||
property bool authenticationRequested: printerConnected && (Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 || Cura.MachineManager.printerOutputDevices[0].authenticationState == 5) // AuthState.AuthenticationRequested or AuthenticationReceived.
|
||||
|
||||
Row
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
332
plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
Normal file
332
plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
Normal file
|
@ -0,0 +1,332 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from UM.Logger import Logger
|
||||
from UM.Application import Application
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Version import Version
|
||||
|
||||
from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from time import time
|
||||
|
||||
import json
|
||||
|
||||
|
||||
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
|
||||
# Zero-Conf is used to detect printers, which are saved in a dict.
|
||||
# If we discover a printer that has the same key as the active machine instance a connection is made.
|
||||
@signalemitter
|
||||
class UM3OutputDevicePlugin(OutputDevicePlugin):
|
||||
addDeviceSignal = Signal()
|
||||
removeDeviceSignal = Signal()
|
||||
discoveredDevicesChanged = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._zero_conf = None
|
||||
self._zero_conf_browser = None
|
||||
|
||||
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
self.addDeviceSignal.connect(self._onAddDevice)
|
||||
self.removeDeviceSignal.connect(self._onRemoveDevice)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
|
||||
|
||||
self._discovered_devices = {}
|
||||
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
self._network_manager.finished.connect(self._onNetworkRequestFinished)
|
||||
|
||||
self._min_cluster_version = Version("4.0.0")
|
||||
|
||||
self._api_version = "1"
|
||||
self._api_prefix = "/api/v" + self._api_version + "/"
|
||||
self._cluster_api_version = "1"
|
||||
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
|
||||
|
||||
# Get list of manual instances from preferences
|
||||
self._preferences = Preferences.getInstance()
|
||||
self._preferences.addPreference("um3networkprinting/manual_instances",
|
||||
"") # A comma-separated list of ip adresses or hostnames
|
||||
|
||||
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
|
||||
|
||||
# The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
|
||||
# which fail to get detailed service info.
|
||||
# Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
|
||||
# them up and process them.
|
||||
self._service_changed_request_queue = Queue()
|
||||
self._service_changed_request_event = Event()
|
||||
self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
|
||||
self._service_changed_request_thread.start()
|
||||
|
||||
def getDiscoveredDevices(self):
|
||||
return self._discovered_devices
|
||||
|
||||
## Start looking for devices on network.
|
||||
def start(self):
|
||||
self.startDiscovery()
|
||||
|
||||
def startDiscovery(self):
|
||||
self.stop()
|
||||
if self._zero_conf_browser:
|
||||
self._zero_conf_browser.cancel()
|
||||
self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed.
|
||||
|
||||
self._zero_conf = Zeroconf()
|
||||
self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
|
||||
[self._appendServiceChangedRequest])
|
||||
|
||||
# Look for manual instances from preference
|
||||
for address in self._manual_instances:
|
||||
if address:
|
||||
self.addManualDevice(address)
|
||||
|
||||
def reCheckConnections(self):
|
||||
active_machine = Application.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
||||
um_network_key = active_machine.getMetaDataEntry("um_network_key")
|
||||
|
||||
for key in self._discovered_devices:
|
||||
if key == um_network_key:
|
||||
if not self._discovered_devices[key].isConnected():
|
||||
Logger.log("d", "Attempting to connect with [%s]" % key)
|
||||
self._discovered_devices[key].connect()
|
||||
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
|
||||
else:
|
||||
if self._discovered_devices[key].isConnected():
|
||||
Logger.log("d", "Attempting to close connection with [%s]" % key)
|
||||
self._discovered_devices[key].close()
|
||||
self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
|
||||
|
||||
def _onDeviceConnectionStateChanged(self, key):
|
||||
if key not in self._discovered_devices:
|
||||
return
|
||||
if self._discovered_devices[key].isConnected():
|
||||
self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
|
||||
else:
|
||||
self.getOutputDeviceManager().removeOutputDevice(key)
|
||||
|
||||
def stop(self):
|
||||
if self._zero_conf is not None:
|
||||
Logger.log("d", "zeroconf close...")
|
||||
self._zero_conf.close()
|
||||
|
||||
def removeManualDevice(self, key, address = None):
|
||||
if key in self._discovered_devices:
|
||||
if not address:
|
||||
address = self._printers[key].ipAddress
|
||||
self._onRemoveDevice(key)
|
||||
|
||||
if address in self._manual_instances:
|
||||
self._manual_instances.remove(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
def addManualDevice(self, address):
|
||||
if address not in self._manual_instances:
|
||||
self._manual_instances.append(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
instance_name = "manual:%s" % address
|
||||
properties = {
|
||||
b"name": address.encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"incomplete": b"true"
|
||||
}
|
||||
|
||||
if instance_name not in self._discovered_devices:
|
||||
# Add a preliminary printer instance
|
||||
self._onAddDevice(instance_name, address, properties)
|
||||
|
||||
self._checkManualDevice(address)
|
||||
|
||||
def _checkManualDevice(self, address):
|
||||
# Check if a UM3 family device exists at this address.
|
||||
# If a printer responds, it will replace the preliminary printer created above
|
||||
# origin=manual is for tracking back the origin of the call
|
||||
url = QUrl("http://" + address + self._api_prefix + "system")
|
||||
name_request = QNetworkRequest(url)
|
||||
self._network_manager.get(name_request)
|
||||
|
||||
def _onNetworkRequestFinished(self, reply):
|
||||
reply_url = reply.url().toString()
|
||||
|
||||
if "system" in reply_url:
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
# Something went wrong with checking the firmware version!
|
||||
return
|
||||
|
||||
try:
|
||||
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except:
|
||||
Logger.log("e", "Something went wrong converting the JSON.")
|
||||
return
|
||||
|
||||
address = reply.url().host()
|
||||
has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
|
||||
instance_name = "manual:%s" % address
|
||||
properties = {
|
||||
b"name": system_info["name"].encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"firmware_version": system_info["firmware"].encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"machine": system_info["variant"].encode("utf-8")
|
||||
}
|
||||
|
||||
if has_cluster_capable_firmware:
|
||||
# Cluster needs an additional request, before it's completed.
|
||||
properties[b"incomplete"] = b"true"
|
||||
|
||||
# Check if the device is still in the list & re-add it with the updated
|
||||
# information.
|
||||
if instance_name in self._discovered_devices:
|
||||
self._onRemoveDevice(instance_name)
|
||||
self._onAddDevice(instance_name, address, properties)
|
||||
|
||||
if has_cluster_capable_firmware:
|
||||
# We need to request more info in order to figure out the size of the cluster.
|
||||
cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
|
||||
cluster_request = QNetworkRequest(cluster_url)
|
||||
self._network_manager.get(cluster_request)
|
||||
|
||||
elif "printers" in reply_url:
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
# Something went wrong with checking the amount of printers the cluster has!
|
||||
return
|
||||
# So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
|
||||
try:
|
||||
cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except:
|
||||
Logger.log("e", "Something went wrong converting the JSON.")
|
||||
return
|
||||
address = reply.url().host()
|
||||
instance_name = "manual:%s" % address
|
||||
if instance_name in self._discovered_devices:
|
||||
device = self._discovered_devices[instance_name]
|
||||
properties = device.getProperties().copy()
|
||||
if b"incomplete" in properties:
|
||||
del properties[b"incomplete"]
|
||||
properties[b'cluster_size'] = len(cluster_printers_list)
|
||||
self._onRemoveDevice(instance_name)
|
||||
self._onAddDevice(instance_name, address, properties)
|
||||
|
||||
def _onRemoveDevice(self, device_id):
|
||||
device = self._discovered_devices.pop(device_id, None)
|
||||
if device:
|
||||
if device.isConnected():
|
||||
device.disconnect()
|
||||
try:
|
||||
device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
|
||||
except TypeError:
|
||||
# Disconnect already happened.
|
||||
pass
|
||||
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
def _onAddDevice(self, name, address, properties):
|
||||
# Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
|
||||
# or "Legacy" UM3 device.
|
||||
cluster_size = int(properties.get(b"cluster_size", -1))
|
||||
|
||||
if cluster_size >= 0:
|
||||
device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
|
||||
else:
|
||||
device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)
|
||||
|
||||
self._discovered_devices[device.getId()] = device
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
|
||||
device.connect()
|
||||
device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
|
||||
|
||||
## Appends a service changed request so later the handling thread will pick it up and processes it.
|
||||
def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
|
||||
# append the request and set the event so the event handling thread can pick it up
|
||||
item = (zeroconf, service_type, name, state_change)
|
||||
self._service_changed_request_queue.put(item)
|
||||
self._service_changed_request_event.set()
|
||||
|
||||
def _handleOnServiceChangedRequests(self):
|
||||
while True:
|
||||
# Wait for the event to be set
|
||||
self._service_changed_request_event.wait(timeout = 5.0)
|
||||
|
||||
# Stop if the application is shutting down
|
||||
if Application.getInstance().isShuttingDown():
|
||||
return
|
||||
|
||||
self._service_changed_request_event.clear()
|
||||
|
||||
# Handle all pending requests
|
||||
reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled
|
||||
while not self._service_changed_request_queue.empty():
|
||||
request = self._service_changed_request_queue.get()
|
||||
zeroconf, service_type, name, state_change = request
|
||||
try:
|
||||
result = self._onServiceChanged(zeroconf, service_type, name, state_change)
|
||||
if not result:
|
||||
reschedule_requests.append(request)
|
||||
except Exception:
|
||||
Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
|
||||
service_type, name)
|
||||
reschedule_requests.append(request)
|
||||
|
||||
# Re-schedule the failed requests if any
|
||||
if reschedule_requests:
|
||||
for request in reschedule_requests:
|
||||
self._service_changed_request_queue.put(request)
|
||||
|
||||
## Handler for zeroConf detection.
|
||||
# Return True or False indicating if the process succeeded.
|
||||
# Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread.
|
||||
def _onServiceChanged(self, zero_conf, service_type, name, state_change):
|
||||
if state_change == ServiceStateChange.Added:
|
||||
Logger.log("d", "Bonjour service added: %s" % name)
|
||||
|
||||
# First try getting info from zero-conf cache
|
||||
info = ServiceInfo(service_type, name, properties={})
|
||||
for record in zero_conf.cache.entries_with_name(name.lower()):
|
||||
info.update_record(zero_conf, time(), record)
|
||||
|
||||
for record in zero_conf.cache.entries_with_name(info.server):
|
||||
info.update_record(zero_conf, time(), record)
|
||||
if info.address:
|
||||
break
|
||||
|
||||
# Request more data if info is not complete
|
||||
if not info.address:
|
||||
Logger.log("d", "Trying to get address of %s", name)
|
||||
info = zero_conf.get_service_info(service_type, name)
|
||||
|
||||
if info:
|
||||
type_of_device = info.properties.get(b"type", None)
|
||||
if type_of_device:
|
||||
if type_of_device == b"printer":
|
||||
address = '.'.join(map(lambda n: str(n), info.address))
|
||||
self.addDeviceSignal.emit(str(name), address, info.properties)
|
||||
else:
|
||||
Logger.log("w",
|
||||
"The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
|
||||
else:
|
||||
Logger.log("w", "Could not get information about %s" % name)
|
||||
return False
|
||||
|
||||
elif state_change == ServiceStateChange.Removed:
|
||||
Logger.log("d", "Bonjour service removed: %s" % name)
|
||||
self.removeDeviceSignal.emit(str(name))
|
||||
|
||||
return True
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from . import NetworkPrinterOutputDevicePlugin
|
||||
|
||||
from . import DiscoverUM3Action
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from . import UM3OutputDevicePlugin
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
|
||||
return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
|
66
plugins/USBPrinting/AutoDetectBaudJob.py
Normal file
66
plugins/USBPrinting/AutoDetectBaudJob.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
|
||||
from .avr_isp.stk500v2 import Stk500v2
|
||||
|
||||
from time import time, sleep
|
||||
from serial import Serial, SerialException
|
||||
|
||||
|
||||
# An async job that attempts to find the correct baud rate for a USB printer.
|
||||
# It tries a pre-set list of baud rates. All these baud rates are validated by requesting the temperature a few times
|
||||
# and checking if the results make sense. If getResult() is not None, it was able to find a correct baud rate.
|
||||
class AutoDetectBaudJob(Job):
|
||||
def __init__(self, serial_port):
|
||||
super().__init__()
|
||||
self._serial_port = serial_port
|
||||
self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
|
||||
|
||||
def run(self):
|
||||
Logger.log("d", "Auto detect baud rate started.")
|
||||
timeout = 3
|
||||
|
||||
programmer = Stk500v2()
|
||||
serial = None
|
||||
try:
|
||||
programmer.connect(self._serial_port)
|
||||
serial = programmer.leaveISP()
|
||||
except:
|
||||
programmer.close()
|
||||
|
||||
for baud_rate in self._all_baud_rates:
|
||||
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
|
||||
|
||||
if serial is None:
|
||||
try:
|
||||
serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout)
|
||||
except SerialException as e:
|
||||
Logger.logException("w", "Unable to create serial")
|
||||
continue
|
||||
else:
|
||||
# We already have a serial connection, just change the baud rate.
|
||||
try:
|
||||
serial.baudrate = baud_rate
|
||||
except:
|
||||
continue
|
||||
sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
|
||||
successful_responses = 0
|
||||
|
||||
serial.write(b"\n") # Ensure we clear out previous responses
|
||||
serial.write(b"M105\n")
|
||||
|
||||
timeout_time = time() + timeout
|
||||
|
||||
while timeout_time > time():
|
||||
line = serial.readline()
|
||||
if b"ok T:" in line:
|
||||
successful_responses += 1
|
||||
if successful_responses >= 3:
|
||||
self.setResult(baud_rate)
|
||||
return
|
||||
|
||||
serial.write(b"M105\n")
|
||||
self.setResult(None) # Unable to detect the correct baudrate.
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Copyright (c) 2017 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
|
@ -34,44 +34,22 @@ UM.Dialog
|
|||
}
|
||||
|
||||
text: {
|
||||
if (manager.errorCode == 0)
|
||||
switch (manager.firmwareUpdateState)
|
||||
{
|
||||
if (manager.firmwareUpdateCompleteStatus)
|
||||
{
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update completed.")
|
||||
}
|
||||
else if (manager.progress == 0)
|
||||
{
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Starting firmware update, this may take a while.")
|
||||
}
|
||||
else
|
||||
{
|
||||
//: Firmware update status label
|
||||
case 0:
|
||||
return "" //Not doing anything (eg; idling)
|
||||
case 1:
|
||||
return catalog.i18nc("@label","Updating firmware.")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (manager.errorCode)
|
||||
{
|
||||
case 1:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
|
||||
case 2:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
|
||||
case 3:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
|
||||
case 4:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
|
||||
default:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label", "Unknown error code: %1").arg(manager.errorCode)
|
||||
}
|
||||
case 2:
|
||||
return catalog.i18nc("@label","Firmware update completed.")
|
||||
case 3:
|
||||
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
|
||||
case 4:
|
||||
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
|
||||
case 5:
|
||||
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
|
||||
case 6:
|
||||
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,16 +59,15 @@ UM.Dialog
|
|||
ProgressBar
|
||||
{
|
||||
id: prog
|
||||
value: manager.firmwareUpdateCompleteStatus ? 100 : manager.progress
|
||||
value: manager.firmwareProgress
|
||||
minimumValue: 0
|
||||
maximumValue: 100
|
||||
indeterminate: (manager.progress < 1) && (!manager.firmwareUpdateCompleteStatus)
|
||||
indeterminate: manager.firmwareProgress < 1 && manager.firmwareProgress > 0
|
||||
anchors
|
||||
{
|
||||
left: parent.left;
|
||||
right: parent.right;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SystemPalette
|
||||
|
|
68
plugins/USBPrinting/USBPrinterOutputController.py
Normal file
68
plugins/USBPrinting/USBPrinterOutputController.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class USBPrinterOuptutController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
super().__init__(output_device)
|
||||
|
||||
self._preheat_bed_timer = QTimer()
|
||||
self._preheat_bed_timer.setSingleShot(True)
|
||||
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
||||
self._preheat_printer = None
|
||||
|
||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
||||
self._output_device.sendCommand("G91")
|
||||
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
|
||||
self._output_device.sendCommand("G90")
|
||||
|
||||
def homeHead(self, printer):
|
||||
self._output_device.sendCommand("G28 X")
|
||||
self._output_device.sendCommand("G28 Y")
|
||||
|
||||
def homeBed(self, printer):
|
||||
self._output_device.sendCommand("G28 Z")
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
if state == "pause":
|
||||
self._output_device.pausePrint()
|
||||
job.updateState("paused")
|
||||
elif state == "print":
|
||||
self._output_device.resumePrint()
|
||||
job.updateState("printing")
|
||||
elif state == "abort":
|
||||
self._output_device.cancelPrint()
|
||||
pass
|
||||
|
||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
||||
try:
|
||||
temperature = round(temperature) # The API doesn't allow floating point.
|
||||
duration = round(duration)
|
||||
except ValueError:
|
||||
return # Got invalid values, can't pre-heat.
|
||||
|
||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
||||
self._preheat_bed_timer.start()
|
||||
self._preheat_printer = printer
|
||||
printer.updateIsPreheating(True)
|
||||
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
||||
self.preheatBed(printer, temperature=0, duration=0)
|
||||
self._preheat_bed_timer.stop()
|
||||
printer.updateIsPreheating(False)
|
||||
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
||||
self._output_device.sendCommand("M140 S%s" % temperature)
|
||||
|
||||
def _onPreheatBedTimerFinished(self):
|
||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
||||
self._preheat_printer.updateIsPreheating(False)
|
File diff suppressed because it is too large
Load diff
|
@ -2,33 +2,32 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from . import USBPrinterOutputDevice
|
||||
from UM.Application import Application
|
||||
from UM.Resources import Resources
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from cura.PrinterOutputDevice import ConnectionState
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from cura.PrinterOutputDevice import ConnectionState
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from . import USBPrinterOutputDevice
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
import threading
|
||||
import platform
|
||||
import time
|
||||
import os.path
|
||||
import serial.tools.list_ports
|
||||
from UM.Extension import Extension
|
||||
|
||||
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer.
|
||||
## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.
|
||||
@signalemitter
|
||||
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
||||
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
||||
addUSBOutputDeviceSignal = Signal()
|
||||
progressChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent = parent)
|
||||
self._serial_port_list = []
|
||||
|
@ -38,39 +37,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
self._update_thread.setDaemon(True)
|
||||
|
||||
self._check_updates = True
|
||||
self._firmware_view = None
|
||||
|
||||
Application.getInstance().applicationShuttingDown.connect(self.stop)
|
||||
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
|
||||
addUSBOutputDeviceSignal = Signal()
|
||||
connectionStateChanged = pyqtSignal()
|
||||
|
||||
progressChanged = pyqtSignal()
|
||||
firmwareUpdateChange = pyqtSignal()
|
||||
|
||||
@pyqtProperty(float, notify = progressChanged)
|
||||
def progress(self):
|
||||
progress = 0
|
||||
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
|
||||
progress += device.progress
|
||||
return progress / len(self._usb_output_devices)
|
||||
|
||||
@pyqtProperty(int, notify = progressChanged)
|
||||
def errorCode(self):
|
||||
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
|
||||
if device._error_code:
|
||||
return device._error_code
|
||||
return 0
|
||||
|
||||
## Return True if all printers finished firmware update
|
||||
@pyqtProperty(float, notify = firmwareUpdateChange)
|
||||
def firmwareUpdateCompleteStatus(self):
|
||||
complete = True
|
||||
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
|
||||
if not device.firmwareUpdateFinished:
|
||||
complete = False
|
||||
return complete
|
||||
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice)
|
||||
|
||||
def start(self):
|
||||
self._check_updates = True
|
||||
|
@ -79,58 +49,28 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
def stop(self):
|
||||
self._check_updates = False
|
||||
|
||||
def _updateThread(self):
|
||||
while self._check_updates:
|
||||
result = self.getSerialPortList(only_list_usb = True)
|
||||
self._addRemovePorts(result)
|
||||
time.sleep(5)
|
||||
|
||||
## Show firmware interface.
|
||||
# This will create the view if its not already created.
|
||||
def spawnFirmwareInterface(self, serial_port):
|
||||
if self._firmware_view is None:
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
|
||||
self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
|
||||
self._firmware_view.show()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateAllFirmware(self, file_name):
|
||||
if file_name.startswith("file://"):
|
||||
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
|
||||
|
||||
if not self._usb_output_devices:
|
||||
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
|
||||
def _onConnectionStateChanged(self, serial_port):
|
||||
if serial_port not in self._usb_output_devices:
|
||||
return
|
||||
|
||||
for printer_connection in self._usb_output_devices:
|
||||
self._usb_output_devices[printer_connection].resetFirmwareUpdate()
|
||||
self.spawnFirmwareInterface("")
|
||||
for printer_connection in self._usb_output_devices:
|
||||
try:
|
||||
self._usb_output_devices[printer_connection].updateFirmware(file_name)
|
||||
except FileNotFoundError:
|
||||
# Should only happen in dev environments where the resources/firmware folder is absent.
|
||||
self._usb_output_devices[printer_connection].setProgress(100, 100)
|
||||
Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name)
|
||||
Message(i18n_catalog.i18nc("@info",
|
||||
"Could not find firmware required for the printer at %s.") % printer_connection, title = i18n_catalog.i18nc("@info:title", "Printer Firmware")).show()
|
||||
self._firmware_view.close()
|
||||
changed_device = self._usb_output_devices[serial_port]
|
||||
if changed_device.connectionState == ConnectionState.connected:
|
||||
self.getOutputDeviceManager().addOutputDevice(changed_device)
|
||||
else:
|
||||
self.getOutputDeviceManager().removeOutputDevice(serial_port)
|
||||
|
||||
def _updateThread(self):
|
||||
while self._check_updates:
|
||||
container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if container_stack is None:
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
@pyqtSlot(str, str, result = bool)
|
||||
def updateFirmwareBySerial(self, serial_port, file_name):
|
||||
if serial_port in self._usb_output_devices:
|
||||
self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort())
|
||||
try:
|
||||
self._usb_output_devices[serial_port].updateFirmware(file_name)
|
||||
except FileNotFoundError:
|
||||
self._firmware_view.close()
|
||||
Logger.log("e", "Could not find firmware required for this machine called '%s'", file_name)
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
if container_stack.getMetaDataEntry("supports_usb_connection"):
|
||||
port_list = self.getSerialPortList(only_list_usb=True)
|
||||
else:
|
||||
port_list = [] # Just use an empty list; all USB devices will be removed.
|
||||
self._addRemovePorts(port_list)
|
||||
time.sleep(5)
|
||||
|
||||
## Return the singleton instance of the USBPrinterManager
|
||||
@classmethod
|
||||
|
@ -191,7 +131,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
Logger.log("w", "There is no firmware for machine %s.", machine_id)
|
||||
|
||||
if hex_file:
|
||||
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
|
||||
try:
|
||||
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
|
||||
except FileNotFoundError:
|
||||
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
|
||||
return ""
|
||||
else:
|
||||
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
|
||||
return ""
|
||||
|
@ -205,46 +149,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
continue
|
||||
self._serial_port_list = list(serial_ports)
|
||||
|
||||
devices_to_remove = []
|
||||
for port, device in self._usb_output_devices.items():
|
||||
if port not in self._serial_port_list:
|
||||
device.close()
|
||||
devices_to_remove.append(port)
|
||||
|
||||
for port in devices_to_remove:
|
||||
del self._usb_output_devices[port]
|
||||
|
||||
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
def addOutputDevice(self, serial_port):
|
||||
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
|
||||
device.connectionStateChanged.connect(self._onConnectionStateChanged)
|
||||
device.connect()
|
||||
device.progressChanged.connect(self.progressChanged)
|
||||
device.firmwareUpdateChange.connect(self.firmwareUpdateChange)
|
||||
self._usb_output_devices[serial_port] = device
|
||||
|
||||
## If one of the states of the connected devices change, we might need to add / remove them from the global list.
|
||||
def _onConnectionStateChanged(self, serial_port):
|
||||
success = True
|
||||
try:
|
||||
if self._usb_output_devices[serial_port].connectionState == ConnectionState.connected:
|
||||
self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port])
|
||||
else:
|
||||
success = success and self.getOutputDeviceManager().removeOutputDevice(serial_port)
|
||||
if success:
|
||||
self.connectionStateChanged.emit()
|
||||
except KeyError:
|
||||
Logger.log("w", "Connection state of %s changed, but it was not found in the list")
|
||||
|
||||
@pyqtProperty(QObject , notify = connectionStateChanged)
|
||||
def connectedPrinterList(self):
|
||||
self._usb_output_devices_model = ListModel()
|
||||
self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name")
|
||||
self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer")
|
||||
for connection in self._usb_output_devices:
|
||||
if self._usb_output_devices[connection].connectionState == ConnectionState.connected:
|
||||
self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]})
|
||||
return self._usb_output_devices_model
|
||||
device.connect()
|
||||
|
||||
## Create a list of serial ports on the system.
|
||||
# \param only_list_usb If true, only usb ports are listed
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import USBPrinterOutputDeviceManager
|
||||
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
|
||||
from PyQt5.QtQml import qmlRegisterSingletonType
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
# We are violating the QT API here (as we use a factory, which is technically not allowed).
|
||||
# but we don't really have another means for doing this (and it seems to you know -work-)
|
||||
qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
|
||||
return {"extension":USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance(), "output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}
|
||||
return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}
|
||||
|
|
|
@ -14,6 +14,9 @@ import Cura 1.0 as Cura
|
|||
Cura.MachineAction
|
||||
{
|
||||
anchors.fill: parent;
|
||||
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
|
||||
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
|
||||
|
||||
Item
|
||||
{
|
||||
id: upgradeFirmwareMachineAction
|
||||
|
@ -60,16 +63,17 @@ Cura.MachineAction
|
|||
{
|
||||
id: autoUpgradeButton
|
||||
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
|
||||
enabled: parent.firmwareName != ""
|
||||
enabled: parent.firmwareName != "" && activeOutputDevice
|
||||
onClicked:
|
||||
{
|
||||
Cura.USBPrinterManager.updateAllFirmware(parent.firmwareName)
|
||||
activeOutputDevice.updateFirmware(parent.firmwareName)
|
||||
}
|
||||
}
|
||||
Button
|
||||
{
|
||||
id: manualUpgradeButton
|
||||
text: catalog.i18nc("@action:button", "Upload custom Firmware");
|
||||
enabled: activeOutputDevice != null
|
||||
onClicked:
|
||||
{
|
||||
customFirmwareDialog.open()
|
||||
|
@ -83,7 +87,7 @@ Cura.MachineAction
|
|||
title: catalog.i18nc("@title:window", "Select custom firmware")
|
||||
nameFilters: "Firmware image files (*.hex)"
|
||||
selectExisting: true
|
||||
onAccepted: Cura.USBPrinterManager.updateAllFirmware(fileUrl)
|
||||
onAccepted: activeOutputDevice.updateFirmware(fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
from . import BedLevelMachineAction
|
||||
from . import UpgradeFirmwareMachineAction
|
||||
from . import UMOCheckupMachineAction
|
||||
from . import UMOUpgradeSelection
|
||||
from . import UM2UpgradeSelection
|
||||
|
||||
|
@ -18,7 +17,6 @@ def register(app):
|
|||
return { "machine_action": [
|
||||
BedLevelMachineAction.BedLevelMachineAction(),
|
||||
UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(),
|
||||
UMOCheckupMachineAction.UMOCheckupMachineAction(),
|
||||
UMOUpgradeSelection.UMOUpgradeSelection(),
|
||||
UM2UpgradeSelection.UM2UpgradeSelection()
|
||||
]}
|
||||
|
|
|
@ -47,9 +47,9 @@
|
|||
"default_value": 30
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"default_value": ";Sliced at: {day} {date} {time}\nM104 S{material_print_temperature} ;set temperatures\nM140 S{material_bed_temperature}\nM109 S{material_print_temperature} ;wait for temperatures\nM190 S{material_bed_temperature}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 Z0 ;move Z to min endstops\nG28 X0 Y0 ;move X/Y to min endstops\nG29 ;Auto Level\nG1 Z0.6 F{travel_speed} ;move the Nozzle near the Bed\nG92 E0\nG1 Y0 ;zero the extruded length\nG1 X10 E30 F500 ;printing a Line from right to left\nG92 E0 ;zero the extruded length again\nG1 Z2\nG1 F{travel_speed}\nM117 Printing...;Put printing message on LCD screen\nM150 R255 U255 B255 P4 ;Change LED Color to white" },
|
||||
"default_value": ";Sliced at: {day} {date} {time}\nM104 S{material_print_temperature} ;set temperatures\nM140 S{material_bed_temperature}\nM109 S{material_print_temperature} ;wait for temperatures\nM190 S{material_bed_temperature}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 Z0 ;move Z to min endstops\nG28 X0 Y0 ;move X/Y to min endstops\nG29 ;Auto Level\nG1 Z0.6 F{speed_travel} ;move the Nozzle near the Bed\nG92 E0\nG1 Y0 ;zero the extruded length\nG1 X10 E30 F500 ;printing a Line from right to left\nG92 E0 ;zero the extruded length again\nG1 Z2\nG1 F{speed_travel}\nM117 Printing...;Put printing message on LCD screen\nM150 R255 U255 B255 P4 ;Change LED Color to white" },
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\nG28 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning"
|
||||
"default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\nG28 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,10 @@
|
|||
"default_value": "RepRap"
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"default_value": ";Sliced at: {day} {date} {time}\n;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}\n;Print time: {print_time}\n;Filament used: {filament_amount}m {filament_weight}g\n;Filament cost: {filament_cost}\n;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line\n;M109 S{print_temperature} ;Uncomment to add your own temperature line\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to max endstops\nG1 Z115.0 F{travel_speed} ;move th e platform up 20mm\nG28 Z0 ;move Z to max endstop\nG1 Z15.0 F{travel_speed} ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F{travel_speed}\nM301 H1 P26.38 I2.57 D67.78\n;Put printing message on LCD screen\nM117 Printing..."
|
||||
"default_value": ";Sliced at: {day} {date} {time}\n;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density}\n;Print time: {print_time}\n;Filament used: {filament_amount}m {filament_weight}g\n;Filament cost: {filament_cost}\n;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line\n;M109 S{print_temperature} ;Uncomment to add your own temperature line\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to max endstops\nG1 Z115.0 F{speed_travel} ;move th e platform up 20mm\nG28 Z0 ;move Z to max endstop\nG1 Z15.0 F{speed_travel} ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F{speed_travel}\nM301 H1 P26.38 I2.57 D67.78\n;Put printing message on LCD screen\nM117 Printing..."
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": ";End GCode\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nG28 Z0\nM84 ;steppers off\nG90 ;absolute positioning\n;{profile_string}"
|
||||
"default_value": ";End GCode\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nG28 Z0\nM84 ;steppers off\nG90 ;absolute positioning\n;{profile_string}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,55 +1,69 @@
|
|||
{
|
||||
"version":2,
|
||||
"name":"Anycubic i3 Mega",
|
||||
"inherits":"fdmprinter",
|
||||
"metadata":{
|
||||
"visible":true,
|
||||
"author":"TheTobby",
|
||||
"manufacturer":"Anycubic",
|
||||
"file_formats":"text/x-gcode",
|
||||
"icon":"icon_ultimaker2",
|
||||
"platform":"anycubic_i3_mega_platform.stl",
|
||||
"has_materials": false,
|
||||
"has_machine_quality": true,
|
||||
"preferred_quality": "*normal*"
|
||||
"version": 2,
|
||||
"name": "Anycubic i3 Mega",
|
||||
"inherits": "fdmprinter",
|
||||
"metadata":
|
||||
{
|
||||
"visible": true,
|
||||
"author": "TheTobby",
|
||||
"manufacturer": "Anycubic",
|
||||
"file_formats": "text/x-gcode",
|
||||
"icon": "icon_ultimaker2",
|
||||
"platform": "anycubic_i3_mega_platform.stl",
|
||||
"has_materials": false,
|
||||
"has_machine_quality": true,
|
||||
"preferred_quality": "*normal*"
|
||||
},
|
||||
|
||||
"overrides":{
|
||||
"machine_name":{
|
||||
"default_value":"Anycubic i3 Mega"
|
||||
|
||||
"overrides":
|
||||
{
|
||||
"machine_name":
|
||||
{
|
||||
"default_value": "Anycubic i3 Mega"
|
||||
},
|
||||
"machine_heated_bed":{
|
||||
"default_value":true
|
||||
"machine_heated_bed":
|
||||
{
|
||||
"default_value": true
|
||||
},
|
||||
"machine_width":{
|
||||
"default_value":210
|
||||
"machine_width":
|
||||
{
|
||||
"default_value": 210
|
||||
},
|
||||
"machine_height":{
|
||||
"default_value":205
|
||||
"machine_height":
|
||||
{
|
||||
"default_value": 205
|
||||
},
|
||||
"machine_depth":{
|
||||
"default_value":210
|
||||
"machine_depth":
|
||||
{
|
||||
"default_value": 210
|
||||
},
|
||||
"machine_center_is_zero":{
|
||||
"default_value":false
|
||||
"machine_center_is_zero":
|
||||
{
|
||||
"default_value": false
|
||||
},
|
||||
"machine_nozzle_size":{
|
||||
"default_value":0.4
|
||||
"machine_nozzle_size":
|
||||
{
|
||||
"default_value": 0.4
|
||||
},
|
||||
"material_diameter":{
|
||||
"default_value":1.75
|
||||
"material_diameter":
|
||||
{
|
||||
"default_value": 1.75
|
||||
},
|
||||
"gantry_height":{
|
||||
"default_value":0
|
||||
"gantry_height":
|
||||
{
|
||||
"default_value": 0
|
||||
},
|
||||
"machine_gcode_flavor":{
|
||||
"default_value":"RepRap (Marlin/Sprinter)"
|
||||
"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 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 F{travel_speed} ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F{travel_speed}\nM117 Printing...\nG5"
|
||||
"machine_start_gcode":
|
||||
{
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 F{speed_travel} ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F{speed_travel}\nM117 Printing...\nG5"
|
||||
},
|
||||
"machine_end_gcode":{
|
||||
"default_value":"M104 S0 ; turn off extruder\nM140 S0 ; turn off bed\nM84 ; disable motors\nM107\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle\nto release some of the pressure\nG1 Z+0.5 E-5 ;X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\nG28 X0 ;Y0 ;move X/Y to min endstops\nso the head is out of the way\nG1 Y180 F2000\nM84 ;steppers off\nG90\nM300 P300 S4000"
|
||||
"machine_end_gcode":
|
||||
{
|
||||
"default_value": "M104 S0 ; turn off extruder\nM140 S0 ; turn off bed\nM84 ; disable motors\nM107\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle\nto release some of the pressure\nG1 Z+0.5 E-5 ;X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\nG28 X0 ;Y0 ;move X/Y to min endstops\nso the head is out of the way\nG1 Y180 F2000\nM84 ;steppers off\nG90\nM300 P300 S4000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"overrides": {
|
||||
"machine_start_gcode": {
|
||||
"default_value": "; -- START GCODE --\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 \nG29 Z0.12 ;Auto-bedleveling with Z offset \nG92 E0 ;zero the extruded length \nG1 F2000 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F{travel_speed}\nM117 Printing...\n; -- end of START GCODE --"
|
||||
"default_value": "; -- START GCODE --\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 \nG29 Z0.12 ;Auto-bedleveling with Z offset \nG92 E0 ;zero the extruded length \nG1 F2000 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F{speed_travel}\nM117 Printing...\n; -- end of START GCODE --"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "; -- START GCODE --\nG28 ; Home all axes\nM104 S0 ;extruder heater off\n;M140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n;M84 ;steppers off\nG90 ;absolute positioning\n; -- end of START GCODE --"
|
||||
|
|
|
@ -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,
|
||||
|
@ -3854,12 +3854,11 @@
|
|||
"support_tree_branch_diameter_angle":
|
||||
{
|
||||
"label": "Tree Support Branch Diameter Angle",
|
||||
"description": "The angle of the branches' diameter as they gradually become thicker towards the bottom. An angle of 0 will cause the branches to have uniform thickness over their length. A negative angle makes them thinner towards the bottom, so be careful as they might disappear. A small positive angle can increase stability of the tree support.",
|
||||
"description": "The angle of the branches' diameter as they gradually become thicker towards the bottom. An angle of 0 will cause the branches to have uniform thickness over their length. A bit of an angle can increase stability of the tree support.",
|
||||
"unit": "°",
|
||||
"type": "float",
|
||||
"minimum_value": "-89.9999",
|
||||
"minimum_value": "0",
|
||||
"maximum_value": "89.9999",
|
||||
"minimum_value_warning": "0",
|
||||
"maximum_value_warning": "15",
|
||||
"default_value": 5,
|
||||
"limit_to_extruder": "support_infill_extruder_nr",
|
||||
|
@ -4234,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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5262,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",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"id": "malyan_m180",
|
||||
"version": 2,
|
||||
"name": "Malyan M180",
|
||||
"inherits": "fdmprinter",
|
85
resources/definitions/malyan_m200.def.json
Normal file
85
resources/definitions/malyan_m200.def.json
Normal file
|
@ -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}
|
||||
}
|
||||
}
|
18
resources/definitions/monoprice_select_mini_v1.def.json
Normal file
18
resources/definitions/monoprice_select_mini_v1.def.json
Normal file
|
@ -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" }
|
||||
}
|
||||
}
|
25
resources/definitions/monoprice_select_mini_v2.def.json
Normal file
25
resources/definitions/monoprice_select_mini_v2.def.json
Normal file
|
@ -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" }
|
||||
}
|
||||
}
|
|
@ -8,46 +8,59 @@
|
|||
"manufacturer": "Tevo",
|
||||
"file_formats": "text/x-gcode",
|
||||
"icon": "icon_ultimaker2",
|
||||
"has_materials": false,
|
||||
"has_materials": false,
|
||||
"has_machine_quality": true,
|
||||
"platform": "prusai3_platform.stl",
|
||||
"preferred_quality": "*normal*"
|
||||
"platform": "tevo_blackwidow.stl",
|
||||
"preferred_quality": "*normal*"
|
||||
},
|
||||
"overrides": {
|
||||
"machine_name": {
|
||||
"overrides":
|
||||
{
|
||||
"machine_name":
|
||||
{
|
||||
"default_value": "Tevo Black Widow"
|
||||
},
|
||||
"machine_heated_bed": {
|
||||
"machine_heated_bed":
|
||||
{
|
||||
"default_value": true
|
||||
},
|
||||
"machine_width": {
|
||||
"machine_width":
|
||||
{
|
||||
"default_value": 350
|
||||
},
|
||||
"machine_height": {
|
||||
"machine_height":
|
||||
{
|
||||
"default_value": 250
|
||||
},
|
||||
"machine_depth": {
|
||||
"machine_depth":
|
||||
{
|
||||
"default_value": 250
|
||||
},
|
||||
"machine_center_is_zero": {
|
||||
"machine_center_is_zero":
|
||||
{
|
||||
"default_value": false
|
||||
},
|
||||
"machine_nozzle_size": {
|
||||
"machine_nozzle_size":
|
||||
{
|
||||
"default_value": 0.4
|
||||
},
|
||||
"material_diameter": {
|
||||
"default_value": 1.75
|
||||
"material_diameter":
|
||||
{
|
||||
"default_value": 1.75
|
||||
},
|
||||
"gantry_height": {
|
||||
"gantry_height":
|
||||
{
|
||||
"default_value": 0
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"machine_gcode_flavor":
|
||||
{
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"machine_start_gcode":
|
||||
{
|
||||
"default_value": "M280 P0 S160 ; release BLTouch alarm (OK to send for Non BLTouch)\nM420 Z2 ; set fade leveling at 2mm for BLTouch (OK to send for Non BLTouch)\nG28 ; home all\nG29 ; probe bed\nG92 E0 ;zero the extruded length\nG1 X0.0 Y50.0 Z10.0 F3600\n; perform wipe and prime\nG1 Z0.0 F1000\nG1 Z0.2 Y70.0 E9.0 F1000.0 ; prime\nG1 Y100.0 E12.5 F1000.0 ; prime\nG92 E0 ; zero extruder again\nM117 Printing..."
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"machine_end_gcode":
|
||||
{
|
||||
"default_value": "G92 E0 ; zero the extruded length again\nG1 E-1.5 F500 ; retract the filament to release some of the pressure\nM104 S0 ; turn off extruder\nM140 S0 ; turn off bed\nG28 X0 ; home X axis\nG1 Y245 ; move Y axis to end position\nM84 ; disable motors\nM107 ; turn off fan"
|
||||
}
|
||||
}
|
||||
|
|
BIN
resources/meshes/malyan_m200_platform.stl
Normal file
BIN
resources/meshes/malyan_m200_platform.stl
Normal file
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue