Merge branch 'master' into feature_multiple_BP

This commit is contained in:
Diego Prado Gesto 2018-01-16 09:59:21 +01:00
commit da4c98b204
194 changed files with 6801 additions and 5633 deletions

View file

@ -12,7 +12,7 @@ class CameraImageProvider(QQuickImageProvider):
def requestImage(self, id, size): def requestImage(self, id, size):
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
try: try:
return output_device.getCameraImage(), QSize(15, 15) return output_device.activePrinter.camera.getImage(), QSize(15, 15)
except AttributeError: except AttributeError:
pass pass
return QImage(), QSize(15, 15) return QImage(), QSize(15, 15)

View file

@ -73,7 +73,7 @@ class CuraActions(QObject):
# \param count The number of times to multiply the selection. # \param count The number of times to multiply the selection.
@pyqtSlot(int) @pyqtSlot(int)
def multiplySelection(self, count: int) -> None: def multiplySelection(self, count: int) -> None:
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, 8) job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8)
job.start() job.start()
## Delete all selected objects. ## Delete all selected objects.

View file

@ -266,6 +266,7 @@ class CuraApplication(QtApplication):
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity) self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
self.getController().toolOperationStopped.connect(self._onToolOperationStopped) self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
self.getController().contextMenuRequested.connect(self._onContextMenuRequested) self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity)
Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware") Resources.addType(self.ResourceTypes.Firmware, "firmware")
@ -646,10 +647,10 @@ class CuraApplication(QtApplication):
if parsed_args["help"]: if parsed_args["help"]:
parser.print_help() parser.print_help()
sys.exit(0) sys.exit(0)
def run(self): def run(self):
self.preRun() self.preRun()
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
self._setUpSingleInstanceServer() self._setUpSingleInstanceServer()
@ -804,6 +805,7 @@ class CuraApplication(QtApplication):
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel") qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel) qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
@ -890,12 +892,18 @@ class CuraApplication(QtApplication):
def getSceneBoundingBoxString(self): 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()} 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): def updatePlatformActivity(self, node = None):
count = 0 count = 0
scene_bounding_box = None scene_bounding_box = None
is_block_slicing_node = False is_block_slicing_node = False
active_build_plate = self.getBuildPlateModel().activeBuildPlate
for node in DepthFirstIterator(self.getController().getScene().getRoot()): 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 continue
if node.callDecoration("isBlockSlicing"): if node.callDecoration("isBlockSlicing"):
is_block_slicing_node = True is_block_slicing_node = True
@ -915,7 +923,7 @@ class CuraApplication(QtApplication):
if not scene_bounding_box: if not scene_bounding_box:
scene_bounding_box = AxisAlignedBox.Null 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._scene_bounding_box = scene_bounding_box
self.sceneBoundingBoxChanged.emit() self.sceneBoundingBoxChanged.emit()
@ -1012,7 +1020,7 @@ class CuraApplication(QtApplication):
Selection.clear() Selection.clear()
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1034,10 +1042,12 @@ class CuraApplication(QtApplication):
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) not in {SceneNode, CuraSceneNode}: if not isinstance(node, SceneNode):
continue continue
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): 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. 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"): if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
nodes.append(node) nodes.append(node)
@ -1050,7 +1060,11 @@ class CuraApplication(QtApplication):
op.push() op.push()
Selection.clear() 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. ## Reset all translation on nodes with mesh data.
@pyqtSlot() @pyqtSlot()
@ -1058,7 +1072,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Resetting all scene translations") Logger.log("i", "Resetting all scene translations")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1086,7 +1100,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Resetting all scene transformations") Logger.log("i", "Resetting all scene transformations")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1113,7 +1127,7 @@ class CuraApplication(QtApplication):
def arrangeObjectsToAllBuildPlates(self): def arrangeObjectsToAllBuildPlates(self):
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1134,7 +1148,7 @@ class CuraApplication(QtApplication):
nodes = [] nodes = []
active_build_plate = self.getBuildPlateModel().activeBuildPlate active_build_plate = self.getBuildPlateModel().activeBuildPlate
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1158,7 +1172,7 @@ class CuraApplication(QtApplication):
# What nodes are on the build plate and are not being moved # What nodes are on the build plate and are not being moved
fixed_nodes = [] fixed_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
@ -1186,7 +1200,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Reloading all loaded mesh data.") Logger.log("i", "Reloading all loaded mesh data.")
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): 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 continue
nodes.append(node) nodes.append(node)
@ -1421,16 +1435,20 @@ class CuraApplication(QtApplication):
filename = job.getFileName() filename = job.getFileName()
self._currently_loading_files.remove(filename) self._currently_loading_files.remove(filename)
root = self.getController().getScene().getRoot()
arranger = Arrange.create(scene_root = root)
min_offset = 8
self.fileLoaded.emit(filename) self.fileLoaded.emit(filename)
arrange_objects_on_load = ( arrange_objects_on_load = (
not Preferences.getInstance().getValue("cura/use_multi_build_plate") or not Preferences.getInstance().getValue("cura/use_multi_build_plate") or
not Preferences.getInstance().getValue("cura/not_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 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: for original_node in nodes:
# Create a CuraSceneNode just if the original node is not that type # Create a CuraSceneNode just if the original node is not that type
@ -1479,7 +1497,14 @@ class CuraApplication(QtApplication):
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher # 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, _ = 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 = AddSceneNodeOperation(node, scene.getRoot())
op.push() op.push()

View file

@ -28,7 +28,7 @@ class ObjectsModel(ListModel):
active_build_plate_number = self._build_plate_number active_build_plate_number = self._build_plate_number
group_nr = 1 group_nr = 1
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode): if not isinstance(node, SceneNode):
continue continue
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
continue continue

View file

@ -18,7 +18,7 @@ class OneAtATimeIterator(Iterator.Iterator):
def _fillStack(self): def _fillStack(self):
node_list = [] node_list = []
for node in self._scene_node.getChildren(): for node in self._scene_node.getChildren():
if not type(node) is SceneNode: if not isinstance(node, SceneNode):
continue continue
if node.callDecoration("getConvexHull"): if node.callDecoration("getConvexHull"):

View file

@ -61,7 +61,7 @@ class PlatformPhysics:
random.shuffle(nodes) random.shuffle(nodes)
for node in 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 continue
bbox = node.getBoundingBox() bbox = node.getBoundingBox()

View file

@ -11,6 +11,7 @@ from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from typing import Dict
import math import math
import os.path import os.path
@ -177,7 +178,7 @@ class PrintInformation(QObject):
self._material_amounts = material_amounts self._material_amounts = material_amounts
self._calculateInformation(build_plate_number) 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 total_estimated_time = 0
if build_plate_number not in self._print_time_message_values: if build_plate_number not in self._print_time_message_values:

View 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()

View 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

View 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()

View 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

View 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)

View 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")

View 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

View file

View file

@ -3,15 +3,21 @@
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice 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 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.Logger import Logger
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Application import Application 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") i18n_catalog = i18nCatalog("cura")
## Printer output device adds extra interface options on top of output device. ## Printer output device adds extra interface options on top of output device.
@ -25,682 +31,149 @@ i18n_catalog = i18nCatalog("cura")
# For all other uses it should be used in the same way as a "regular" OutputDevice. # For all other uses it should be used in the same way as a "regular" OutputDevice.
@signalemitter @signalemitter
class PrinterOutputDevice(QObject, OutputDevice): 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): def __init__(self, device_id, parent = None):
super().__init__(device_id = device_id, parent = parent) super().__init__(device_id = device_id, parent = parent)
self._container_registry = ContainerRegistry.getInstance() self._printers = [] # type: List[PrinterOutputModel]
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._buildplate_id = ""
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._monitor_view_qml_path = "" self._monitor_view_qml_path = ""
self._monitor_component = None
self._monitor_item = None self._monitor_item = None
self._control_view_qml_path = "" self._control_view_qml_path = ""
self._control_component = None
self._control_item = None self._control_item = None
self._qml_context = None self._qml_context = None
self._can_pause = True self._accepts_commands = False
self._can_abort = True
self._can_pre_heat_bed = True
self._can_control_manually = True
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") 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 @pyqtProperty("QVariantList", notify = printersChanged)
bedTemperatureChanged = pyqtSignal() def printers(self):
return self._printers
# 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 to be emitted when the buildplate is changed
buildplateChanged = pyqtSignal()
# 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(QObject, constant=True) @pyqtProperty(QObject, constant=True)
def monitorItem(self): def monitorItem(self):
# Note that we specifically only check if the monitor component is created. # 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 # 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. # create the item (and fail) every time.
if not self._monitor_item: if not self._monitor_component:
self._createMonitorViewFromQML() self._createMonitorViewFromQML()
return self._monitor_item return self._monitor_item
@pyqtProperty(QObject, constant=True) @pyqtProperty(QObject, constant=True)
def controlItem(self): def controlItem(self):
if not self._control_item: if not self._control_component:
self._createControlViewFromQML() self._createControlViewFromQML()
return self._control_item return self._control_item
def _createControlViewFromQML(self): def _createControlViewFromQML(self):
if not self._control_view_qml_path: if not self._control_view_qml_path:
return return
if self._control_item is None:
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, { self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
"OutputDevice": self
})
def _createMonitorViewFromQML(self): def _createMonitorViewFromQML(self):
if not self._monitor_view_qml_path: if not self._monitor_view_qml_path:
return return
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, { if self._monitor_item is None:
"OutputDevice": self 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)
@pyqtProperty(str, notify = buildplateChanged)
def buildplateId(self):
return self._buildplate_id
## Protected setter for the current buildplate id.
# /param buildplate_id id of the buildplate
def _setBuildplateId(self, buildplate_id):
if buildplate_id and buildplate_id != self._buildplate_id:
Logger.log("d", "Setting buildplate id to %s." % (buildplate_id))
self._buildplate_id = buildplate_id
self.buildplateChanged.emit(buildplate_id)
elif not buildplate_id:
Logger.log("d", "Removing buildplate id.")
self._buildplate_id = None
self.buildplateChanged.emit(None)
## Let the user decide if the hotends and/or material should be synced with the printer
# NB: the UX needs to be implemented by the plugin
def materialHotendChangedMessage(self, callback):
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
callback(QMessageBox.Yes)
## Attempt to establish connection ## Attempt to establish connection
def connect(self): def connect(self):
raise NotImplementedError("connect needs to be implemented") self.setConnectionState(ConnectionState.connecting)
self._update_timer.start()
## Attempt to close the connection ## Attempt to close the connection
def close(self): def close(self):
raise NotImplementedError("close needs to be implemented") self._update_timer.stop()
self.setConnectionState(ConnectionState.closed)
@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()
## Ensure that close gets called when object is destroyed ## Ensure that close gets called when object is destroyed
def __del__(self): def __del__(self):
self.close() self.close()
## Get the x position of the head. @pyqtProperty(bool, notify=acceptsCommandsChanged)
# This function is "final" (do not re-implement) def acceptsCommands(self):
@pyqtProperty(float, notify = headPositionChanged) return self._accepts_commands
def headX(self):
return self._head_x
## Get the y position of the head. ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
# This function is "final" (do not re-implement) def _setAcceptsCommands(self, accepts_commands):
@pyqtProperty(float, notify = headPositionChanged) if self._accepts_commands != accepts_commands:
def headY(self): self._accepts_commands = accepts_commands
return self._head_y
## Get the z position of the head. self.acceptsCommandsChanged.emit()
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headZ(self):
return self._head_z
## Update the saved position of the head
# This function should be called when a new position for the head is received.
def _updateHeadPosition(self, x, y ,z):
position_changed = False
if self._head_x != x:
self._head_x = x
position_changed = True
if self._head_y != y:
self._head_y = y
position_changed = True
if self._head_z != z:
self._head_z = z
position_changed = True
if position_changed:
self.headPositionChanged.emit()
## Set the position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadPosition implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def setHeadPosition(self, x, y, z, speed = 3000):
self._setHeadPosition(x, y , z, speed)
## Set the X position of the head.
# This function is "final" (do not re-implement)
# /param x x position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadx implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadX(self, x, speed = 3000):
self._setHeadX(x, speed)
## Set the Y position of the head.
# This function is "final" (do not re-implement)
# /param y y position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadY(self, y, speed = 3000):
self._setHeadY(y, speed)
## Set the Z position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param z z position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadZ(self, z, speed = 3000):
self._setHeadZ(z, speed)
## Move the head of the printer.
# Note that this is a relative move. If you want to move the head to a specific position you can use
# setHeadPosition
# This function is "final" (do not re-implement)
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _moveHead implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
self._moveHead(x, y, z, speed)
## Implementation function of moveHead.
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa moveHead
def _moveHead(self, x, y, z, speed):
Logger.log("w", "_moveHead is not implemented by this output device")
## Implementation function of setHeadPosition.
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadPosition
def _setHeadPosition(self, x, y, z, speed):
Logger.log("w", "_setHeadPosition is not implemented by this output device")
## Implementation function of setHeadX.
# /param x new x location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadX
def _setHeadX(self, x, speed):
Logger.log("w", "_setHeadX is not implemented by this output device")
## Implementation function of setHeadY.
# /param y new y location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY
def _setHeadY(self, y, speed):
Logger.log("w", "_setHeadY is not implemented by this output device")
## Implementation function of setHeadZ.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ
def _setHeadZ(self, z, speed):
Logger.log("w", "_setHeadZ is not implemented by this output device")
## Get the progress of any currently active process.
# This function is "final" (do not re-implement)
# /sa _getProgress
# /returns float progress of the process. -1 indicates that there is no process.
@pyqtProperty(float, notify = progressChanged)
def progress(self):
return self._progress
## Set the progress of any currently active process
# /param progress Progress of the process.
def setProgress(self, progress):
if self._progress != progress:
self._progress = progress
self.progressChanged.emit()
## The current processing state of the backend. ## The current processing state of the backend.
@ -709,4 +182,4 @@ class ConnectionState(IntEnum):
connecting = 1 connecting = 1
connected = 2 connected = 2
busy = 3 busy = 3
error = 4 error = 4

View file

@ -136,6 +136,9 @@ class QualityManager:
if basic_materials: if basic_materials:
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria) result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
empty_quality = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
result.append(empty_quality)
return result return result
## Find all quality changes for a machine. ## Find all quality changes for a machine.

View file

@ -13,7 +13,7 @@ class BuildPlateDecorator(SceneNodeDecorator):
# Make sure that groups are set correctly # Make sure that groups are set correctly
# setBuildPlateForSelection in CuraActions makes sure that no single childs are set. # setBuildPlateForSelection in CuraActions makes sure that no single childs are set.
self._build_plate_number = nr 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 self._node.transformChanged() # trigger refresh node without introducing a new signal
if self._node and self._node.callDecoration("isGroup"): if self._node and self._node.callDecoration("isGroup"):
for child in self._node.getChildren(): for child in self._node.getChildren():

View file

@ -65,7 +65,7 @@ class ConvexHullNode(SceneNode):
ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6)
if self.getParent(): 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) renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
if self._convex_hull_head_mesh: 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) renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)

View file

@ -10,9 +10,12 @@ from UM.Application import Application
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Signal import Signal
class CuraSceneController(QObject): class CuraSceneController(QObject):
activeBuildPlateChanged = Signal()
def __init__(self, objects_model: ObjectsModel, build_plate_model: BuildPlateModel): def __init__(self, objects_model: ObjectsModel, build_plate_model: BuildPlateModel):
super().__init__() super().__init__()
@ -30,7 +33,7 @@ class CuraSceneController(QObject):
source = args[0] source = args[0]
else: else:
source = None source = None
if not issubclass(type(source), SceneNode): if not isinstance(source, SceneNode):
return return
max_build_plate = self._calcMaxBuildPlate() max_build_plate = self._calcMaxBuildPlate()
changed = False changed = False
@ -101,6 +104,7 @@ class CuraSceneController(QObject):
self._build_plate_model.setActiveBuildPlate(nr) self._build_plate_model.setActiveBuildPlate(nr)
self._objects_model.setActiveBuildPlate(nr) self._objects_model.setActiveBuildPlate(nr)
self.activeBuildPlateChanged.emit()
@staticmethod @staticmethod
def createCuraSceneController(): def createCuraSceneController():

View file

@ -816,6 +816,22 @@ class ContainerManager(QObject):
ContainerRegistry.getInstance().addContainer(container_to_add) ContainerRegistry.getInstance().addContainer(container_to_add)
return self._getMaterialContainerIdForActiveMachine(clone_of_original) 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 ## 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. # \return \type{str} the id of the newly created container.

View file

@ -202,7 +202,6 @@ class CuraContainerRegistry(ContainerRegistry):
for plugin_id, meta_data in self._getIOPlugins("profile_reader"): for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
if meta_data["profile_reader"][0]["extension"] != extension: if meta_data["profile_reader"][0]["extension"] != extension:
continue continue
profile_reader = plugin_registry.getPluginObject(plugin_id) profile_reader = plugin_registry.getPluginObject(plugin_id)
try: try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. 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._id = new_id
profile.setName(new_name) 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(): if "type" in profile.getMetaData():
profile.setMetaDataEntry("type", "quality_changes") profile.setMetaDataEntry("type", "quality_changes")
else: else:
@ -515,6 +518,7 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id) extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id)
if extruder_quality_changes_container: if extruder_quality_changes_container:
extruder_quality_changes_container = extruder_quality_changes_container[0] extruder_quality_changes_container = extruder_quality_changes_container[0]
quality_changes_id = extruder_quality_changes_container.getId() quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id) extruder_stack.setQualityChangesById(quality_changes_id)
else: else:
@ -525,15 +529,92 @@ class CuraContainerRegistry(ContainerRegistry):
if extruder_quality_changes_container: if extruder_quality_changes_container:
quality_changes_id = extruder_quality_changes_container.getId() quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id) 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: if not extruder_quality_changes_container:
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
machine.qualityChanges.getName(), extruder_stack.getId()) 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: else:
extruder_stack.setQualityChangesById("empty_quality_changes") extruder_stack.setQualityChangesById("empty_quality_changes")
self.addContainer(extruder_stack) 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 # Set next stack at the end
extruder_stack.setNextStack(machine) extruder_stack.setNextStack(machine)
@ -562,6 +643,9 @@ class CuraContainerRegistry(ContainerRegistry):
if parser["general"]["name"] == name: if parser["general"]["name"] == name:
# load the container # load the container
container_id = os.path.basename(file_path).replace(".inst.cfg", "") 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) instance_container = InstanceContainer(container_id)
with open(file_path, "r") as f: with open(file_path, "r") as f:

View file

@ -118,7 +118,7 @@ class MachineManager(QObject):
self._auto_hotends_changed = {} self._auto_hotends_changed = {}
self._material_incompatible_message = Message(catalog.i18nc("@info:status", 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")) title = catalog.i18nc("@info:title", "Incompatible Material"))
containers = ContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) containers = ContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId)
@ -136,7 +136,7 @@ class MachineManager(QObject):
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
stacksValidationChanged = pyqtSignal() # Emitted whenever a validation 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() outputDevicesChanged = pyqtSignal()
@ -145,8 +145,7 @@ class MachineManager(QObject):
printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged) printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged)
printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged) printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)
self._printer_output_devices.clear() self._printer_output_devices = []
for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
if isinstance(printer_output_device, PrinterOutputDevice): if isinstance(printer_output_device, PrinterOutputDevice):
self._printer_output_devices.append(printer_output_device) self._printer_output_devices.append(printer_output_device)
@ -175,58 +174,70 @@ class MachineManager(QObject):
def totalNumberOfSettings(self) -> int: def totalNumberOfSettings(self) -> int:
return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys()) return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys())
def _onHotendIdChanged(self, index: Union[str, int], hotend_id: str) -> None: def _onHotendIdChanged(self):
if not self._global_container_stack: 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 return
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "variant", definition = self._global_container_stack.definition.getId(), name = hotend_id) change_found = False
if containers: # New material ID is known machine_id = self.activeMachineId
extruder_manager = ExtruderManager.getInstance() extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
machine_id = self.activeMachineId key=lambda k: k.getMetaDataEntry("position"))
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))
def _onMaterialIdChanged(self, index: Union[str, int], material_id: str): for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
if not self._global_container_stack: 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 return
definition_id = "fdmprinter" active_printer_model = self._printer_output_devices[0].activePrinter
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False): if not active_printer_model:
definition_id = self.activeQualityDefinitionId return
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
if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id: change_found = False
# Save the material that needs to be changed. Multiple changes will be handled by the callback. machine_id = self.activeMachineId
if self._global_container_stack.definition.getMetaDataEntry("has_variants") and matching_extruder.variant: extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
variant_id = self.getQualityVariantId(self._global_container_stack.definition, matching_extruder.variant) key=lambda k: k.getMetaDataEntry("position"))
for container in containers:
if container.get("variant") == variant_id: for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
self._auto_materials_changed[str(index)] = container["id"] if extruder_model.activeMaterial is None:
break continue
else: containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="material",
# Just use the first result we found. definition=self._global_container_stack.definition.getId(),
self._auto_materials_changed[str(index)] = containers[0]["id"] GUID=extruder_model.activeMaterial.guid)
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback) if containers:
else: # The material is known.
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id)) 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): def _materialHotendChangedCallback(self, button):
if button == QMessageBox.No: if button == QMessageBox.No:

View file

@ -36,6 +36,8 @@ class ProfilesModel(InstanceContainersModel):
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update) Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
Application.getInstance().getMachineManager().activeMaterialChanged.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 # Factory function, used by QML
@staticmethod @staticmethod
def createProfilesModel(engine, js_engine): def createProfilesModel(engine, js_engine):
@ -85,13 +87,10 @@ class ProfilesModel(InstanceContainersModel):
if quality.getMetaDataEntry("quality_type") not in quality_type_set: if quality.getMetaDataEntry("quality_type") not in quality_type_set:
result.append(quality) result.append(quality)
# if still profiles are found, add a single empty_quality ("Not supported") instance to the drop down list if len(result) > 1:
if len(result) == 0: result.remove(self._empty_quality)
# 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)
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. ## Re-computes the items in this model, and adds the layer height role.
def _recomputeItems(self): def _recomputeItems(self):
@ -114,7 +113,6 @@ class ProfilesModel(InstanceContainersModel):
# active machine and material, and later yield the right ones. # active machine and material, and later yield the right ones.
tmp_all_quality_items = OrderedDict() tmp_all_quality_items = OrderedDict()
for item in super()._recomputeItems(): for item in super()._recomputeItems():
profiles = container_registry.findContainersMetadata(id = item["id"]) profiles = container_registry.findContainersMetadata(id = item["id"])
if not profiles or "quality_type" not in profiles[0]: if not profiles or "quality_type" not in profiles[0]:
quality_type = "" quality_type = ""

View file

@ -42,5 +42,7 @@ class QualityAndUserProfilesModel(ProfilesModel):
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())} qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())}
result = filtered_quality_changes result = filtered_quality_changes
result.update({q.getId():q for q in quality_list}) 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. 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.

View file

@ -1,8 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import collections
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from UM.Logger import Logger from UM.Logger import Logger
@ -42,6 +40,8 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
self.addRoleName(self.UserValueRole, "user_value") self.addRoleName(self.UserValueRole, "user_value")
self.addRoleName(self.CategoryRole, "category") self.addRoleName(self.CategoryRole, "category")
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
def setExtruderId(self, extruder_id): def setExtruderId(self, extruder_id):
if extruder_id != self._extruder_id: if extruder_id != self._extruder_id:
self._extruder_id = extruder_id self._extruder_id = extruder_id
@ -107,77 +107,87 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
else: else:
quality_changes_container = containers[0] quality_changes_container = containers[0]
criteria = { if quality_changes_container.getMetaDataEntry("quality_type") == "not_supported":
"type": "quality", quality_container = self._empty_quality
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"), else:
"definition": quality_changes_container.getDefinition().getId() criteria = {
} "type": "quality",
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
"definition": quality_changes_container.getDefinition().getId()
}
quality_container = self._container_registry.findInstanceContainers(**criteria) quality_container = self._container_registry.findInstanceContainers(**criteria)
if not quality_container: if not quality_container:
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId()) Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
return return
quality_container = quality_container[0]
quality_container = quality_container[0]
quality_type = quality_container.getMetaDataEntry("quality_type") 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. if quality_type == "not_supported":
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix containers = []
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix)) else:
if catalog.hasTranslationLoaded(): definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
self._i18n_catalog = catalog definition = quality_container.getDefinition()
for file_name in quality_container.getDefinition().getInheritedFiles(): # Check if the definition container has a translation file.
catalog = i18nCatalog(os.path.basename(file_name)) definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
if catalog.hasTranslationLoaded(): if catalog.hasTranslationLoaded():
self._i18n_catalog = catalog 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 = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
criteria["material"] = self._material_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) criteria["extruder"] = self._extruder_id
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 and "material" in criteria:
# Try again, this time without material
criteria.pop("material", None)
containers = self._container_registry.findInstanceContainers(**criteria) 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: if not containers and "material" in criteria:
# Try again, this time without material or extruder # Try again, this time without material
criteria.pop("extruder") # "material" has already been popped criteria.pop("material", None)
containers = self._container_registry.findInstanceContainers(**criteria) containers = self._container_registry.findInstanceContainers(**criteria)
if not containers: if not containers:
Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria)) # Try again, this time without material or extruder
return 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: if quality_changes_container:
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()} if quality_type == "not_supported":
if self._extruder_definition_id != "": criteria = {"type": "quality_changes", "quality_type": quality_type, "name": quality_changes_container.getName()}
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: 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) changes = self._container_registry.findInstanceContainers(**criteria)
if changes: if changes:
containers.extend(changes) containers.extend(changes)
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
current_category = "" current_category = ""
for definition in definition_container.findDefinitions(): 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: if profile_value is None and user_value is None:
continue continue
if is_multi_extrusion: settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
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 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:
if self._extruder_id != "" and not settable_per_extruder: continue
continue
# If a setting is settable per extruder (not global) and we're looking at global tab, don't show this value. # 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: if self._extruder_id == "" and settable_per_extruder:
continue continue
label = definition.label label = definition.label
if self._i18n_catalog: if self._i18n_catalog:

View file

@ -609,7 +609,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container.setName(self._container_registry.uniqueName(instance_container.getName())) instance_container.setName(self._container_registry.uniqueName(instance_container.getName()))
new_changes_container_id = self.getNewId(instance_container.getId()) 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 # TODO: we don't know the following is correct or not, need to verify
# AND REFACTOR!!! # AND REFACTOR!!!

View file

@ -6,8 +6,9 @@ from UM.Math.Vector import Vector
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Application import Application from UM.Application import Application
import UM.Scene.SceneNode from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.CuraApplication import CuraApplication
import Savitar import Savitar
@ -62,11 +63,15 @@ class ThreeMFWriter(MeshWriter):
self._store_archive = store_archive self._store_archive = store_archive
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode ## 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()): 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 return None
active_build_plate_nr = CuraApplication.getInstance().getBuildPlateModel().activeBuildPlate
if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
return
savitar_node = Savitar.SceneNode() savitar_node = Savitar.SceneNode()
node_matrix = um_node.getLocalTransformation() node_matrix = um_node.getLocalTransformation()
@ -97,6 +102,9 @@ class ThreeMFWriter(MeshWriter):
savitar_node.setSetting(key, str(stack.getProperty(key, "value"))) savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
for child_node in um_node.getChildren(): 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) savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
if savitar_child_node is not None: if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node) savitar_node.addChild(savitar_child_node)

View file

@ -88,7 +88,6 @@ class CuraEngineBackend(QObject, Backend):
# #
self._global_container_stack = None self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged() self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@ -238,6 +237,8 @@ class CuraEngineBackend(QObject, Backend):
self._slicing = True self._slicing = True
self.slicingStarted.emit() self.slicingStarted.emit()
self.determineAutoSlicing() # Switch timer on or off if appropriate
slice_message = self._socket.createMessage("cura.proto.Slice") slice_message = self._socket.createMessage("cura.proto.Slice")
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
self._start_slice_job_build_plate = build_plate_to_be_sliced self._start_slice_job_build_plate = build_plate_to_be_sliced
@ -421,7 +422,7 @@ class CuraEngineBackend(QObject, Backend):
# #
# \param source The scene node that was changed. # \param source The scene node that was changed.
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
if not issubclass(type(source), SceneNode): if not isinstance(source, SceneNode):
return return
build_plate_changed = set() build_plate_changed = set()
@ -458,6 +459,7 @@ class CuraEngineBackend(QObject, Backend):
for build_plate_number in build_plate_changed: for build_plate_number in build_plate_changed:
if build_plate_number not in self._build_plates_to_be_sliced: if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number) self._build_plates_to_be_sliced.append(build_plate_number)
self.printDurationMessage.emit(source_build_plate_number, {}, [])
self.processingProgress.emit(0.0) self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
# if not self._use_timer: # if not self._use_timer:
@ -520,7 +522,7 @@ class CuraEngineBackend(QObject, Backend):
def _onStackErrorCheckFinished(self): def _onStackErrorCheckFinished(self):
self._is_error_check_scheduled = False 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.needsSlicing()
self._onChanged() self._onChanged()
@ -583,8 +585,10 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "See if there is more to slice...") Logger.log("d", "See if there is more to slice...")
# Somehow this results in an Arcus Error # Somehow this results in an Arcus Error
# self.slice() # self.slice()
# Testing call slice again, allow backend to restart by using the timer # Call slice again using the timer, allowing the backend to restart
self._invokeSlice() 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. ## Called when a g-code message is received from the engine.
# #
@ -717,7 +721,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack.containersChanged.disconnect(self._onChanged) 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: for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingChanged) extruder.propertyChanged.disconnect(self._onSettingChanged)
@ -728,7 +732,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack: 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.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
self._global_container_stack.containersChanged.connect(self._onChanged) 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: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingChanged) extruder.propertyChanged.connect(self._onSettingChanged)
extruder.containersChanged.connect(self._onChanged) extruder.containersChanged.connect(self._onChanged)

View file

@ -4,7 +4,6 @@
import gc import gc
from UM.Job import Job from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.Application import Application from UM.Application import Application
from UM.Mesh.MeshData import MeshData from UM.Mesh.MeshData import MeshData
from UM.Preferences import Preferences from UM.Preferences import Preferences
@ -17,6 +16,7 @@ from UM.Logger import Logger
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura import LayerDataBuilder from cura import LayerDataBuilder
from cura import LayerDataDecorator from cura import LayerDataDecorator
@ -81,7 +81,7 @@ class ProcessSlicedLayersJob(Job):
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
new_node = SceneNode() new_node = CuraSceneNode()
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
# Force garbage collection. # Force garbage collection.

View file

@ -64,8 +64,8 @@ class GCodeWriter(MeshWriter):
gcode_dict = getattr(scene, "gcode_dict") gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict: if not gcode_dict:
return False return False
gcode_list = gcode_dict.get(active_build_plate) gcode_list = gcode_dict.get(active_build_plate, None)
if gcode_list: if gcode_list is not None:
for gcode in gcode_list: for gcode in gcode_list:
stream.write(gcode) stream.write(gcode)
# Serialise the current container stack and put it at the end of the file. # Serialise the current container stack and put it at the end of the file.

View file

@ -8,8 +8,9 @@ import Cura 1.0 as Cura
Item Item
{ {
width: parent.width // parent could be undefined as this component is not visible at all times
height: parent.height 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 // We show a nice overlay on the 3D viewer when the current output device has no monitor view
Rectangle Rectangle

View file

@ -14,60 +14,122 @@ class MonitorStage(CuraStage):
super().__init__(parent) super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail # 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 self._active_print_job = None
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource) self._active_printer = None
def _setComponents(self): def _setActivePrintJob(self, print_job):
self._setMainOverlay() if self._active_print_job != print_job:
self._setSidebar() if self._active_print_job:
self._setIconSource() 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") main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path) 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! # 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") sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path) self.addDisplayComponent("sidebar", sidebar_component_path)
def _setIconSource(self): def _updateIconSource(self):
if Application.getInstance().getTheme() is not None: if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon() icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name)) self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state ## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self): def _getActiveOutputDeviceStatusIcon(self):
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice() # We assume that you are monitoring the device with the highest priority.
try:
if not output_device: output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
except IndexError:
return "tab_status_unknown" return "tab_status_unknown"
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands: if not output_device.acceptsCommands:
return "tab_status_unknown" return "tab_status_unknown"
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"): if output_device.activePrinter is None:
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", ""]:
return "tab_status_connected" 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" return "tab_status_paused"
if output_device.jobState == "error": if output_device.activePrinter.activePrintJob.state == "error":
return "tab_status_stopped" return "tab_status_stopped"
return "tab_status_unknown" return "tab_status_unknown"

View file

@ -22,7 +22,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
self._selected_object_id = None self._selected_object_id = None
self._node = None self._node = None
self._stack = None self._stack = None
self._skip_setting = None
# this is a set of settings that will be skipped if the user chooses to reset.
self._skip_reset_setting_set = set()
def setSelectedObjectId(self, id): def setSelectedObjectId(self, id):
if id != self._selected_object_id: if id != self._selected_object_id:
@ -39,8 +41,8 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
return self._selected_object_id return self._selected_object_id
@pyqtSlot(str) @pyqtSlot(str)
def setSkipSetting(self, setting_name): def addSkipResetSetting(self, setting_name):
self._skip_setting = setting_name self._skip_reset_setting_set.add(setting_name)
def setVisible(self, visible): def setVisible(self, visible):
if not self._node: if not self._node:
@ -57,7 +59,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
# Remove all instances that are not in visibility list # Remove all instances that are not in visibility list
for instance in all_instances: for instance in all_instances:
# exceptionally skip setting # exceptionally skip setting
if self._skip_setting is not None and self._skip_setting == instance.definition.key: if instance.definition.key in self._skip_reset_setting_set:
continue continue
if instance.definition.key not in visible: if instance.definition.key not in visible:
settings.removeInstance(instance.definition.key) settings.removeInstance(instance.definition.key)

View file

@ -18,6 +18,9 @@ Item {
width: childrenRect.width; width: childrenRect.width;
height: childrenRect.height; 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 Column
{ {
id: items id: items
@ -39,6 +42,13 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
UM.SettingPropertyProvider
{
id: meshTypePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId
watchedProperties: [ "enabled" ]
}
ComboBox ComboBox
{ {
id: meshTypeSelection id: meshTypeSelection
@ -49,36 +59,55 @@ Item {
model: ListModel model: ListModel
{ {
id: meshTypeModel 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({ meshTypeModel.append({
type: "support_mesh", type: "support_mesh",
text: catalog.i18nc("@label", "Print as support") text: catalog.i18nc("@label", "Print as support")
}); });
}
meshTypePropertyProvider.key = "anti_overhang_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({ meshTypeModel.append({
type: "anti_overhang_mesh", type: "anti_overhang_mesh",
text: catalog.i18nc("@label", "Don't support overlap with other models") text: catalog.i18nc("@label", "Don't support overlap with other models")
}); });
}
meshTypePropertyProvider.key = "cutting_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({ meshTypeModel.append({
type: "cutting_mesh", type: "cutting_mesh",
text: catalog.i18nc("@label", "Modify settings for overlap with other models") text: catalog.i18nc("@label", "Modify settings for overlap with other models")
}); });
}
meshTypePropertyProvider.key = "infill_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({ meshTypeModel.append({
type: "infill_mesh", type: "infill_mesh",
text: catalog.i18nc("@label", "Modify settings for infill of other models") text: catalog.i18nc("@label", "Modify settings for infill of other models")
}); });
meshTypeSelection.updateCurrentIndex();
} }
meshTypeSelection.updateCurrentIndex();
} }
function updateCurrentIndex() function updateCurrentIndex()
{ {
var mesh_type = UM.ActiveTool.properties.getValue("MeshType"); var mesh_type = UM.ActiveTool.properties.getValue("MeshType");
meshTypeSelection.currentIndex = -1;
for(var index=0; index < meshTypeSelection.model.count; index++) for(var index=0; index < meshTypeSelection.model.count; index++)
{ {
if(meshTypeSelection.model.get(index).type == mesh_type) if(meshTypeSelection.model.get(index).type == mesh_type)
@ -91,6 +120,16 @@ Item {
} }
} }
Connections
{
target: Cura.MachineManager
onGlobalContainerChanged:
{
meshTypeSelection.model.clear();
meshTypeSelection.populateModel();
}
}
Connections Connections
{ {
target: UM.Selection target: UM.Selection
@ -106,7 +145,7 @@ Item {
id: currentSettings id: currentSettings
property int maximumHeight: 200 * screenScaleFactor property int maximumHeight: 200 * screenScaleFactor
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight) 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 ScrollView
{ {
@ -124,7 +163,15 @@ Item {
id: addedSettingsModel; id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ] 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 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,17 +373,18 @@ Item {
id: settingPickDialog id: settingPickDialog
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model") title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
width: screenScaleFactor * 360; width: screenScaleFactor * 360
property string labelFilter: "" property string labelFilter: ""
property var additional_excluded_settings
onVisibilityChanged: onVisibilityChanged:
{ {
// force updating the model to sync it with addedSettingsModel // force updating the model to sync it with addedSettingsModel
if(visible) if(visible)
{ {
// Set skip setting, it will prevent from restting selected mesh_type // Set skip setting, it will prevent from resetting selected mesh_type
contents.model.visibilityHandler.setSkipSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
listview.model.forceUpdate() listview.model.forceUpdate()
} }
} }
@ -396,7 +455,12 @@ Item {
} }
visibilityHandler: UM.SettingPreferenceVisibilityHandler {} visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ] 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 delegate:Loader
{ {

View file

@ -54,7 +54,11 @@ class PostProcessingPlugin(QObject, Extension):
## Execute all post-processing scripts on the gcode. ## Execute all post-processing scripts on the gcode.
def execute(self, output_device): def execute(self, output_device):
scene = Application.getInstance().getController().getScene() scene = Application.getInstance().getController().getScene()
gcode_dict = getattr(scene, "gcode_dict") gcode_dict = None
if hasattr(scene, "gcode_dict"):
gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict: if not gcode_dict:
return return

View file

@ -106,7 +106,7 @@ class SimulationPass(RenderPass):
nozzle_node = node nozzle_node = node
nozzle_node.setVisible(False) nozzle_node.setVisible(False)
elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible(): elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
layer_data = node.callDecoration("getLayerData") layer_data = node.callDecoration("getLayerData")
if not layer_data: if not layer_data:
continue continue

View file

@ -28,6 +28,7 @@ class SolidView(View):
self._enabled_shader = None self._enabled_shader = None
self._disabled_shader = None self._disabled_shader = None
self._non_printing_shader = None self._non_printing_shader = None
self._support_mesh_shader = None
self._extruders_model = ExtrudersModel() self._extruders_model = ExtrudersModel()
self._theme = None 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_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
self._non_printing_shader.setUniformValue("u_opacity", 0.6) 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() global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: if global_container_stack:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value") 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) renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False): elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader) 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: else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms) renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node): if node.callDecoration("isGroup") and Selection.isSelected(node):

View file

@ -10,13 +10,12 @@ Component
{ {
id: base id: base
property var manager: Cura.MachineManager.printerOutputDevices[0] 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 lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme. property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: manager != null visible: manager != null
anchors.fill: parent
color: UM.Theme.getColor("viewport_background")
UM.I18nCatalog UM.I18nCatalog
{ {
@ -97,7 +96,7 @@ Component
} }
Label Label
{ {
text: manager.numJobsPrinting text: manager.activePrintJobs.length
font: UM.Theme.getFont("small") font: UM.Theme.getFont("small")
anchors.right: parent.right anchors.right: parent.right
} }
@ -114,7 +113,7 @@ Component
} }
Label Label
{ {
text: manager.numJobsQueued text: manager.queuedPrintJobs.length
font: UM.Theme.getFont("small") font: UM.Theme.getFont("small")
anchors.right: parent.right anchors.right: parent.right
} }

View file

@ -12,10 +12,10 @@ Component
width: maximumWidth width: maximumWidth
height: maximumHeight height: maximumHeight
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight") property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme. property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme. property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog UM.I18nCatalog
{ {
id: catalog id: catalog
@ -33,9 +33,9 @@ Component
horizontalCenter: parent.horizontalCenter 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 Item
@ -46,7 +46,7 @@ Component
width: Math.min(800 * screenScaleFactor, maximumWidth) width: Math.min(800 * screenScaleFactor, maximumWidth)
height: children.height height: children.height
visible: OutputDevice.connectedPrinters.length != 0 visible: OutputDevice.printers.length != 0
Label Label
{ {
@ -62,7 +62,6 @@ Component
} }
} }
ScrollView ScrollView
{ {
id: printerScrollView id: printerScrollView
@ -79,7 +78,7 @@ Component
anchors.fill: parent anchors.fill: parent
spacing: -UM.Theme.getSize("default_lining").height spacing: -UM.Theme.getSize("default_lining").height
model: OutputDevice.connectedPrinters model: OutputDevice.printers
delegate: PrinterInfoBlock delegate: PrinterInfoBlock
{ {
@ -95,7 +94,7 @@ Component
PrinterVideoStream PrinterVideoStream
{ {
visible: OutputDevice.selectedPrinterName != "" visible: OutputDevice.activePrinter != null
anchors.fill:parent anchors.fill:parent
} }
} }

View 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

View file

@ -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)

View file

@ -12,7 +12,10 @@ from cura.MachineAction import MachineAction
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class DiscoverUM3Action(MachineAction): class DiscoverUM3Action(MachineAction):
discoveredDevicesChanged = pyqtSignal()
def __init__(self): def __init__(self):
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network")) super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
self._qml_url = "DiscoverUM3Action.qml" self._qml_url = "DiscoverUM3Action.qml"
@ -25,24 +28,24 @@ class DiscoverUM3Action(MachineAction):
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
self._last_zeroconf_event_time = time.time() self._last_zero_conf_event_time = time.time()
self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset
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() @pyqtSlot()
def startDiscovery(self): def startDiscovery(self):
if not self._network_plugin: 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 = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged) self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self.printersChanged.emit() self.discoveredDevicesChanged.emit()
## Re-filters the list of printers. ## Re-filters the list of devices.
@pyqtSlot() @pyqtSlot()
def reset(self): def reset(self):
Logger.log("d", "Reset the list of found printers.") Logger.log("d", "Reset the list of found devices.")
self.printersChanged.emit() self.discoveredDevicesChanged.emit()
@pyqtSlot() @pyqtSlot()
def restartDiscovery(self): 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 # It's most likely that the QML engine is still creating delegates, where the python side already deleted or
# garbage collected the data. # garbage collected the data.
# Whatever the case, waiting a bit ensures that it doesn't crash. # 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: if not self._network_plugin:
self.startDiscovery() self.startDiscovery()
else: else:
self._network_plugin.startDiscovery() self._network_plugin.startDiscovery()
@pyqtSlot(str, str) @pyqtSlot(str, str)
def removeManualPrinter(self, key, address): def removeManualDevice(self, key, address):
if not self._network_plugin: if not self._network_plugin:
return return
self._network_plugin.removeManualPrinter(key, address) self._network_plugin.removeManualDevice(key, address)
@pyqtSlot(str, str) @pyqtSlot(str, str)
def setManualPrinter(self, key, address): def setManualDevice(self, key, address):
if key != "": if key != "":
# This manual printer replaces a current manual printer # This manual printer replaces a current manual printer
self._network_plugin.removeManualPrinter(key) self._network_plugin.removeManualDevice(key)
if address != "": if address != "":
self._network_plugin.addManualPrinter(address) self._network_plugin.addManualDevice(address)
def _onPrinterDiscoveryChanged(self, *args): def _onDeviceDiscoveryChanged(self, *args):
self._last_zeroconf_event_time = time.time() self._last_zero_conf_event_time = time.time()
self.printersChanged.emit() self.discoveredDevicesChanged.emit()
@pyqtProperty("QVariantList", notify = printersChanged) @pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
def foundDevices(self): def foundDevices(self):
if self._network_plugin: if self._network_plugin:
# TODO: Check if this needs to stay.
if Application.getInstance().getGlobalContainerStack(): if Application.getInstance().getGlobalContainerStack():
global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId() global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId()
else: else:
global_printer_type = "unknown" 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. # 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) printers.sort(key = lambda k: k.name)
return printers return printers
else: else:

View file

@ -10,7 +10,7 @@ Cura.MachineAction
{ {
id: base id: base
anchors.fill: parent; anchors.fill: parent;
property var selectedPrinter: null property var selectedDevice: null
property bool completeProperties: true property bool completeProperties: true
Connections Connections
@ -29,9 +29,9 @@ Cura.MachineAction
function connectToPrinter() 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) if(manager.getStoredKey() != printerKey)
{ {
manager.setKey(printerKey); manager.setKey(printerKey);
@ -83,10 +83,10 @@ Cura.MachineAction
{ {
id: editButton id: editButton
text: catalog.i18nc("@action:button", "Edit") 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: 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 id: removeButton
text: catalog.i18nc("@action:button", "Remove") text: catalog.i18nc("@action:button", "Remove")
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true" enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress) onClicked: manager.removeManualDevice(base.selectedDevice.key, base.selectedDevice.ipAddress)
} }
Button Button
@ -139,7 +139,7 @@ Cura.MachineAction
{ {
var selectedKey = manager.getStoredKey(); var selectedKey = manager.getStoredKey();
for(var i = 0; i < model.length; i++) { for(var i = 0; i < model.length; i++) {
if(model[i].getKey() == selectedKey) if(model[i].key == selectedKey)
{ {
currentIndex = i; currentIndex = i;
return return
@ -151,9 +151,9 @@ Cura.MachineAction
currentIndex: -1 currentIndex: -1
onCurrentIndexChanged: 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 // 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() Component.onCompleted: manager.startDiscovery()
delegate: Rectangle delegate: Rectangle
@ -199,13 +199,13 @@ Cura.MachineAction
Column Column
{ {
width: Math.floor(parent.width * 0.5) width: Math.floor(parent.width * 0.5)
visible: base.selectedPrinter ? true : false visible: base.selectedDevice ? true : false
spacing: UM.Theme.getSize("default_margin").height spacing: UM.Theme.getSize("default_margin").height
Label Label
{ {
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.name : "" text: base.selectedDevice ? base.selectedDevice.name : ""
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -226,17 +226,17 @@ Cura.MachineAction
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: 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") return catalog.i18nc("@label", "Ultimaker 3")
} else if(base.selectedPrinter.printerType == "ultimaker3_extended") } else if(base.selectedDevice.printerType == "ultimaker3_extended")
{ {
return catalog.i18nc("@label Printer name", "Ultimaker 3 Extended") return catalog.i18nc("@label", "Ultimaker 3 Extended")
} else } 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 else
@ -255,7 +255,7 @@ Cura.MachineAction
{ {
width: Math.floor(parent.width * 0.5) width: Math.floor(parent.width * 0.5)
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : "" text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
} }
Label Label
{ {
@ -267,7 +267,7 @@ Cura.MachineAction
{ {
width: Math.floor(parent.width * 0.5) width: Math.floor(parent.width * 0.5)
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.ipAddress : "" text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
} }
} }
@ -277,17 +277,17 @@ Cura.MachineAction
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text:{ text:{
// The property cluster size does not exist for older UM3 devices. // 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 ""; 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."); return catalog.i18nc("@label", "This printer is not set up to host a group of Ultimaker 3 printers.");
} }
else 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 width: parent.width
wrapMode: Text.WordWrap 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." ) text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
} }
Button Button
{ {
text: catalog.i18nc("@action:button", "Connect") text: catalog.i18nc("@action:button", "Connect")
enabled: (base.selectedPrinter && base.completeProperties) ? true : false enabled: (base.selectedDevice && base.completeProperties) ? true : false
onClicked: connectToPrinter() onClicked: connectToPrinter()
} }
} }
@ -337,7 +337,7 @@ Cura.MachineAction
onAccepted: onAccepted:
{ {
manager.setManualPrinter(printerKey, addressText) manager.setManualDevice(printerKey, addressText)
} }
Column { Column {

View 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

View file

@ -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

View file

@ -6,40 +6,49 @@ import Cura 1.0 as Cura
Component Component
{ {
Image Item
{ {
id: cameraImage width: maximumWidth
property bool proportionalHeight: 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); onVisibleChanged:
}
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)
{ {
OutputDevice.startCamera() if(visible)
} else {
{ if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
OutputDevice.stopCamera() {
OutputDevice.activePrinter.camera.start()
}
} else
{
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.stop()
}
}
} }
} source:
source:
{
if(OutputDevice.cameraImage)
{ {
return OutputDevice.cameraImage; if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
{
return OutputDevice.activePrinter.camera.latestImage;
}
return "";
} }
return "";
} }
} }
} }

View file

@ -1,736 +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
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")
self._job_list = list(gcode_dict.keys())
Logger.log("d", "build plates to be sent to printer: %s", (self._job_list))
if self._stage != OutputStage.ready:
if self._error_message:
self._error_message.hide()
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
self._add_build_plate_number = len(self._job_list) > 1
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
if len(self._printers) > 1:
self.spawnPrintView() # Ask user how to print it.
elif len(self._printers) == 1:
# If there is only one printer, don't bother asking.
self.selectAutomaticPrinter()
self.sendPrintJob()
else:
# Cluster has no printers, warn the user of this.
if self._error_message:
self._error_message.hide()
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers."))
self._error_message.show()
## Actually send the print job, called from the dialog
# :param: require_printer_name: name of printer, or ""
@pyqtSlot()
def sendPrintJob(self):
nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job
output_build_plate_number = self._job_list.pop(0)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")[output_build_plate_number]
if not gcode_dict: # Empty build plate
Logger.log("d", "Skipping empty job (build plate number %d).", output_build_plate_number)
return self.sendPrintJob()
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
self._send_gcode_start = time.time()
Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number))
if self._stage != OutputStage.ready:
Logger.log("d", "Unable to send print job as the state is %s", self._stage)
raise OutputDeviceError.DeviceBusyError()
self._stage = OutputStage.uploading
if self._add_build_plate_number:
self._file_name = "%s_%d.gcode.gz" % (file_name, output_build_plate_number)
else:
self._file_name = "%s.gcode.gz" % (file_name)
self._showProgressMessage()
require_printer_name = self._selected_printer["unique_name"]
new_request = self._buildSendPrintJobHttpRequest(require_printer_name, gcode_list)
if new_request is None or self._stage != OutputStage.uploading:
return
self._request = new_request
self._reply = self._manager.post(self._request, self._multipart)
self._reply.uploadProgress.connect(self._onUploadProgress)
# See _finishedPrintJobPostRequest()
def _buildSendPrintJobHttpRequest(self, require_printer_name, gcode_list):
api_url = QUrl(self._api_base_uri + "print_jobs/")
request = QNetworkRequest(api_url)
# Create multipart request and add the g-code.
self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType)
# Add gcode
part = QHttpPart()
part.setHeader(QNetworkRequest.ContentDispositionHeader,
'form-data; name="file"; filename="%s"' % (self._file_name))
compressed_gcode = self._compressGcode(gcode_list)
if compressed_gcode is None:
return None # User aborted print, so stop trying.
part.setBody(compressed_gcode)
self._multipart.append(part)
# require_printer_name "" means automatic
if require_printer_name:
self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name))
user_name = self.__get_username()
if user_name is None:
user_name = "unknown"
self._multipart.append(self.__createKeyValueHttpPart("owner", user_name))
self._addUserAgentHeader(request)
return request
def _compressGcode(self, gcode_list):
self._compressing_print = True
batched_line = ""
max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB
byte_array_file_data = b""
def _compressDataAndNotifyQt(data_to_append):
compressed_data = gzip.compress(data_to_append.encode("utf-8"))
self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Pretend that this is a response, as zipping might take a bit of time.
self._last_response_time = time.time()
return compressed_data
if gcode_list is None:
Logger.log("e", "Unable to find sliced gcode, returning empty.")
return byte_array_file_data
for line in gcode_list:
if not self._compressing_print:
self._progress_message.hide()
return None # Stop trying to zip, abort was called.
batched_line += line
# if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
# Compressing line by line in this case is extremely slow, so we need to batch them.
if len(batched_line) < max_chars_per_line:
continue
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
batched_line = ""
# Also compress the leftovers.
if batched_line:
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
return byte_array_file_data
def __createKeyValueHttpPart(self, key, value):
metadata_part = QHttpPart()
metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain')
metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key))
metadata_part.setBody(bytearray(value, "utf8"))
return metadata_part
def __get_username(self):
try:
return getpass.getuser()
except:
Logger.log("d", "Could not get the system user name, returning 'unknown' instead.")
return None
def _finishedPrintJobPostRequest(self, reply):
self._stage = OutputStage.ready
if self._progress_message:
self._progress_message.hide()
self._progress_message = None
self.writeFinished.emit(self)
if reply.error():
self._showRequestFailedMessage(reply)
self.writeError.emit(self)
else:
self._showRequestSucceededMessage()
self.writeSuccess.emit(self)
self._cleanupRequest()
if self._job_list: # start sending next job
self.sendPrintJob()
def _showRequestFailedMessage(self, reply):
if reply is not None:
Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format(
cluster_name = self.getName(),
error_string = str(reply.errorString()),
error = str(reply.error())))
error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.")
message = Message(text=error_message_template.format(
cluster_name = self.getName()))
message.show()
def _showRequestSucceededMessage(self):
confirmation_message_template = i18n_catalog.i18nc(
"@info:status",
"Sent {file_name} to group {cluster_name}."
)
file_name = os.path.basename(self._file_name).split(".")[0]
message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
message = Message(text=message_text)
button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
message.addAction("open_browser", button_text, "globe", button_tooltip)
message.actionTriggered.connect(self._onMessageActionTriggered)
message.show()
def setPrintJobs(self, print_jobs):
#TODO: hack, last seen messes up the check, so drop it.
for job in print_jobs:
del job["last_seen"]
# Strip any extensions
job["name"] = self._removeGcodeExtension(job["name"])
if self._print_jobs != print_jobs:
old_print_jobs = self._print_jobs
self._print_jobs = print_jobs
self._notifyFinishedPrintJobs(old_print_jobs, print_jobs)
self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs)
# Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer
# for some reason. ugh.
self._print_job_by_printer_uuid = {}
self._print_job_by_uuid = {}
for print_job in print_jobs:
if "printer_uuid" in print_job and print_job["printer_uuid"] is not None:
self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job
self._print_job_by_uuid[print_job["uuid"]] = print_job
self.printJobsChanged.emit()
def _removeGcodeExtension(self, name):
parts = name.split(".")
if parts[-1].upper() == "GZ":
parts = parts[:-1]
if parts[-1].upper() == "GCODE":
parts = parts[:-1]
return ".".join(parts)
def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs):
"""Notify the user when any of their print jobs have just completed.
Arguments:
old_print_jobs -- the previous list of print job status information as returned by the cluster REST API.
new_print_jobs -- the current list of print job status information as returned by the cluster REST API.
"""
if old_print_jobs is None:
return
username = self.__get_username()
if username is None:
return
our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs)
our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"]
our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs)
our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"]
old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs])
for print_job in our_new_finished_print_jobs:
if print_job["uuid"] in old_not_finished_print_job_uuids:
printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
if printer_name is None:
printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")
message_text = (i18n_catalog.i18nc("@info:status",
"Printer '{printer_name}' has finished printing '{job_name}'.")
.format(printer_name=printer_name, job_name=print_job["name"]))
message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished"))
Application.getInstance().showMessage(message)
Application.getInstance().showToastMessage(
i18n_catalog.i18nc("@info:status", "Print finished"),
message_text)
def __filterOurPrintJobs(self, print_jobs):
username = self.__get_username()
return [print_job for print_job in print_jobs if print_job["owner"] == username]
def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs):
if old_print_jobs is None:
return
old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs))
new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs))
old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs])
for print_job in new_change_required_print_jobs:
if print_job["uuid"] not in old_change_required_print_job_uuids:
printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"])
if printer_name is None:
# don't report on yet unknown printers
continue
message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.")
.format(printer_name=printer_name, job_name=print_job["name"]))
message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required"))
Application.getInstance().showMessage(message)
Application.getInstance().showToastMessage(
i18n_catalog.i18nc("@label:status", "Action required"),
message_text)
def __filterConfigChangePrintJobs(self, print_jobs):
return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs)
def __isConfigurationChangeRequiredPrintJob(self, print_job):
if print_job["status"] == "queued":
changes_required = print_job.get("configuration_changes_required", [])
return len(changes_required) != 0
return False
def __getPrinterNameFromUuid(self, printer_uuid):
for printer in self._printers:
if printer["uuid"] == printer_uuid:
return printer["friendly_name"]
return None
def setPrinters(self, printers):
if self._printers != printers:
self._connected_printers_type_count = []
printers_count = {}
self._printers = printers
self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name
for printer in printers:
variant = printer["machine_variant"]
if variant in printers_count:
printers_count[variant] += 1
else:
printers_count[variant] = 1
for type in printers_count:
self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]})
self.printersChanged.emit()
@pyqtProperty("QVariantList", notify=printersChanged)
def connectedPrintersTypeCount(self):
return self._connected_printers_type_count
@pyqtProperty("QVariantList", notify=printersChanged)
def connectedPrinters(self):
return self._printers
@pyqtProperty(int, notify=printJobsChanged)
def numJobsPrinting(self):
num_jobs_printing = 0
for job in self._print_jobs:
if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]:
num_jobs_printing += 1
return num_jobs_printing
@pyqtProperty(int, notify=printJobsChanged)
def numJobsQueued(self):
num_jobs_queued = 0
for job in self._print_jobs:
if job["status"] == "queued":
num_jobs_queued += 1
return num_jobs_queued
@pyqtProperty("QVariantMap", notify=printJobsChanged)
def printJobsByUUID(self):
return self._print_job_by_uuid
@pyqtProperty("QVariantMap", notify=printJobsChanged)
def printJobsByPrinterUUID(self):
return self._print_job_by_printer_uuid
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self):
return self._print_jobs
@pyqtProperty("QVariantList", notify=printersChanged)
def printers(self):
return [self._automatic_printer, ] + self._printers
@pyqtSlot(str, str)
def selectPrinter(self, unique_name, friendly_name):
self.stopCamera()
self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name}
Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name)
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if unique_name == "":
self._address = self._master_address
else:
self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"]
self.selectedPrinterChanged.emit()
def _updateJobState(self, job_state):
name = self._selected_printer.get("friendly_name")
if name == "" or name == "Automatic":
# TODO: This is now a bit hacked; If no printer is selected, don't show job state.
if self._job_state != "":
self._job_state = ""
self.jobStateChanged.emit()
else:
if self._job_state != job_state:
self._job_state = job_state
self.jobStateChanged.emit()
@pyqtSlot()
def selectAutomaticPrinter(self):
self.stopCamera()
self._selected_printer = self._automatic_printer
self.selectedPrinterChanged.emit()
@pyqtProperty("QVariant", notify=selectedPrinterChanged)
def selectedPrinterName(self):
return self._selected_printer.get("unique_name", "")
def getPrintJobsUrl(self):
return self._host + "/print_jobs"
def getPrintersUrl(self):
return self._host + "/printers"
def _showProgressMessage(self):
progress_message_template = i18n_catalog.i18nc("@info:progress",
"Sending <filename>{file_name}</filename> to group {cluster_name}")
file_name = os.path.basename(self._file_name).split(".")[0]
self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1)
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
self._progress_message.actionTriggered.connect(self._onMessageActionTriggered)
self._progress_message.show()
def _addUserAgentHeader(self, request):
request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin")
def _cleanupRequest(self):
self._request = None
self._stage = OutputStage.ready
self._file_name = None
def _onFinished(self, reply):
super()._onFinished(reply)
reply_url = reply.url().toString()
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 500:
Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url))
return
if reply.error() == QNetworkReply.ContentOperationNotPermittedError:
# It was probably "/api/v1/materials" for legacy UM3
return
if reply.error() == QNetworkReply.ContentNotFoundError:
# It was probably "/api/v1/print_job" for legacy UM3
return
if reply.operation() == QNetworkAccessManager.PostOperation:
if self._cluster_api_prefix + "print_jobs" in reply_url:
self._finishedPrintJobPostRequest(reply)
return
# We need to do this check *after* we process the post operation!
# If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this.
if reply.error() != QNetworkReply.NoError:
Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error())
return
elif reply.operation() == QNetworkAccessManager.GetOperation:
if self._cluster_api_prefix + "print_jobs" in reply_url:
self._finishedPrintJobsRequest(reply)
elif self._cluster_api_prefix + "printers" in reply_url:
self._finishedPrintersRequest(reply)
@pyqtSlot()
def openPrintJobControlPanel(self):
Logger.log("d", "Opening print job control panel...")
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
@pyqtSlot()
def openPrinterControlPanel(self):
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl(self.getPrintersUrl()))
def _onMessageActionTriggered(self, message, action):
if action == "open_browser":
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
if action == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
self._compressing_print = False
if self._reply:
self._reply.abort()
self._stage = OutputStage.ready
Application.getInstance().getController().setActiveStage("PrepareStage")
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
## For cluster below
def _get_plugin_directory_name(self):
current_file_absolute_path = os.path.realpath(__file__)
directory_path = os.path.dirname(current_file_absolute_path)
_, directory_name = os.path.split(directory_path)
return directory_name
@property
def _plugin_path(self):
return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name())

File diff suppressed because it is too large Load diff

View file

@ -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()))

View file

@ -15,7 +15,7 @@ Item
Label Label
{ {
id: materialLabel id: materialLabel
text: printCoreConfiguration.material.material + " (" + printCoreConfiguration.material.color + ")" text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : ""
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
@ -23,7 +23,7 @@ Item
Label Label
{ {
id: printCoreLabel id: printCoreLabel
text: printCoreConfiguration.print_core_id text: printCoreConfiguration.hotendID
anchors.top: materialLabel.bottom anchors.top: materialLabel.bottom
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width

View file

@ -20,8 +20,24 @@ UM.Dialog
visible: true visible: true
modality: Qt.ApplicationModal 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 Column
{ {
@ -31,8 +47,7 @@ UM.Dialog
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.rightMargin: UM.Theme.getSize("default_margin").width
height: 50 * screenScaleFactor height: 50 * screenScaleFactord
Label Label
{ {
id: manualPrinterSelectionLabel id: manualPrinterSelectionLabel
@ -42,7 +57,7 @@ UM.Dialog
topMargin: UM.Theme.getSize("default_margin").height topMargin: UM.Theme.getSize("default_margin").height
right: parent.right right: parent.right
} }
text: "Printer selection" text: catalog.i18nc("@label", "Printer selection")
wrapMode: Text.Wrap wrapMode: Text.Wrap
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
} }
@ -50,18 +65,12 @@ UM.Dialog
ComboBox ComboBox
{ {
id: printerSelectionCombobox id: printerSelectionCombobox
model: OutputDevice.printers model: base.printersModel
textRole: "friendly_name" textRole: "name"
width: parent.width width: parent.width
height: 40 * screenScaleFactor height: 40 * screenScaleFactor
Behavior on height { NumberAnimation { duration: 100 } } Behavior on height { NumberAnimation { duration: 100 } }
onActivated:
{
var printerData = OutputDevice.printers[index];
OutputDevice.selectPrinter(printerData.unique_name, printerData.friendly_name);
}
} }
SystemPalette SystemPalette
@ -79,8 +88,6 @@ UM.Dialog
enabled: true enabled: true
onClicked: { onClicked: {
base.visible = false; base.visible = false;
// reset to defaults
OutputDevice.selectAutomaticPrinter()
printerSelectionCombobox.currentIndex = 0 printerSelectionCombobox.currentIndex = 0
} }
} }
@ -93,9 +100,8 @@ UM.Dialog
enabled: true enabled: true
onClicked: { onClicked: {
base.visible = false; base.visible = false;
OutputDevice.sendPrintJob(); OutputDevice.sendPrintJob(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
// reset to defaults // reset to defaults
OutputDevice.selectAutomaticPrinter()
printerSelectionCombobox.currentIndex = 0 printerSelectionCombobox.currentIndex = 0
} }
} }

View file

@ -22,16 +22,16 @@ Rectangle
{ {
return ""; return "";
} }
if (printJob.time_total === 0) if (printJob.timeTotal === 0)
{ {
return ""; 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) function printerStatusText(printer)
{ {
switch (printer.status) switch (printer.state)
{ {
case "pre_print": case "pre_print":
return catalog.i18nc("@label", "Preparing to print") return catalog.i18nc("@label", "Preparing to print")
@ -49,31 +49,23 @@ Rectangle
} }
id: printerDelegate 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.width: UM.Theme.getSize("default_lining").width
border.color: mouse.containsMouse ? emphasisColor : lineColor 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. 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 MouseArea
{ {
id: mouse id: mouse
anchors.fill:parent anchors.fill:parent
onClicked: OutputDevice.selectPrinter(printer.unique_name, printer.friendly_name) onClicked: OutputDevice.setActivePrinter(printer)
hoverEnabled: true; hoverEnabled: true;
// Only clickable if no printer is selected // Only clickable if no printer is selected
enabled: OutputDevice.selectedPrinterName == "" && printer.status !== "unreachable" enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
} }
Row Row
@ -122,7 +114,7 @@ Rectangle
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? getPrettyTime(printJob.time_total) : "" text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
opacity: 0.65 opacity: 0.65
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
elide: Text.ElideRight elide: Text.ElideRight
@ -140,7 +132,7 @@ Rectangle
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width) 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") font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -150,7 +142,7 @@ Rectangle
id: printerTypeLabel id: printerTypeLabel
anchors.top: printerNameLabel.bottom anchors.top: printerNameLabel.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width) width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.machine_variant text: printer.type
anchors.left: parent.left anchors.left: parent.left
elide: Text.ElideRight elide: Text.ElideRight
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
@ -166,7 +158,7 @@ Rectangle
anchors.right: printProgressArea.left anchors.right: printProgressArea.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: emphasisColor color: emphasisColor
opacity: printer != null && printer.status === "unreachable" ? 0.3 : 1 opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
Image Image
{ {
@ -192,7 +184,7 @@ Rectangle
{ {
id: leftExtruderInfo id: leftExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2) width: Math.floor((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.configuration[0] printCoreConfiguration: printer.extruders[0]
} }
Rectangle Rectangle
@ -207,7 +199,7 @@ Rectangle
{ {
id: rightExtruderInfo id: rightExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2) width: Math.floor((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.configuration[1] printCoreConfiguration: printer.extruders[1]
} }
} }
@ -225,9 +217,9 @@ Rectangle
if(printJob != null) if(printJob != null)
{ {
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"]; 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 Item // Status and Percent
@ -235,7 +227,7 @@ Rectangle
id: printProgressTitleBar id: printProgressTitleBar
property var showPercent: { 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 width: parent.width
@ -252,19 +244,19 @@ Rectangle
anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (!printer.enabled) if (printer.state == "disabled")
{ {
return catalog.i18nc("@label:status", "Disabled"); return catalog.i18nc("@label:status", "Disabled");
} }
if (printer.status === "unreachable") if (printer.state === "unreachable")
{ {
return printerStatusText(printer); return printerStatusText(printer);
} }
if (printJob != null) if (printJob != null)
{ {
switch (printJob.status) switch (printJob.state)
{ {
case "printing": case "printing":
case "post_print": case "post_print":
@ -277,14 +269,7 @@ Rectangle
case "sent_to_printer": case "sent_to_printer":
return catalog.i18nc("@label", "Preparing to print") return catalog.i18nc("@label", "Preparing to print")
case "queued": case "queued":
if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
{
return catalog.i18nc("@label:status", "Action required"); return catalog.i18nc("@label:status", "Action required");
}
else
{
return "";
}
case "pausing": case "pausing":
case "paused": case "paused":
return catalog.i18nc("@label:status", "Paused"); return catalog.i18nc("@label:status", "Paused");
@ -328,26 +313,23 @@ Rectangle
visible: !printProgressTitleBar.showPercent visible: !printProgressTitleBar.showPercent
source: { source: {
if (!printer.enabled) if (printer.state == "disabled")
{ {
return "blocked-icon.svg"; return "blocked-icon.svg";
} }
if (printer.status === "unreachable") if (printer.state === "unreachable")
{ {
return ""; return "";
} }
if (printJob != null) 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"; return "checkmark-icon.svg";
} }
@ -384,23 +366,23 @@ Rectangle
{ {
text: text:
{ {
if (!printer.enabled) if (printer.state == "disabled")
{ {
return catalog.i18nc("@label", "Not accepting print jobs"); return catalog.i18nc("@label", "Not accepting print jobs");
} }
if (printer.status === "unreachable") if (printer.state === "unreachable")
{ {
return ""; return "";
} }
if(printJob != null) if(printJob != null)
{ {
switch (printJob.status) switch (printJob.state)
{ {
case "printing": case "printing":
case "post_print": 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": case "wait_cleanup":
return catalog.i18nc("@label", "Clear build plate") return catalog.i18nc("@label", "Clear build plate")
case "sent_to_printer": case "sent_to_printer":
@ -409,10 +391,7 @@ Rectangle
case "wait_for_configuration": case "wait_for_configuration":
return catalog.i18nc("@label", "Not accepting print jobs") return catalog.i18nc("@label", "Not accepting print jobs")
case "queued": 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: default:
return ""; return "";
} }
@ -432,7 +411,7 @@ Rectangle
text: { text: {
if(printJob != null) 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) return OutputDevice.getDateCompleted(printJob.time_total - printJob.time_elapsed)
} }

View file

@ -17,7 +17,7 @@ Item
MouseArea MouseArea
{ {
anchors.fill: parent anchors.fill: parent
onClicked: OutputDevice.selectAutomaticPrinter() onClicked: OutputDevice.setActivePrinter(null)
z: 0 z: 0
} }
@ -32,7 +32,7 @@ Item
width: 20 * screenScaleFactor width: 20 * screenScaleFactor
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
onClicked: OutputDevice.selectAutomaticPrinter() onClicked: OutputDevice.setActivePrinter(null)
style: ButtonStyle style: ButtonStyle
{ {
@ -65,17 +65,23 @@ Item
{ {
if(visible) if(visible)
{ {
OutputDevice.startCamera() if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.start()
}
} else } else
{ {
OutputDevice.stopCamera() if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.stop()
}
} }
} }
source: 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 ""; return "";
} }

View file

@ -13,7 +13,7 @@ Item
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3" property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands 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 Row
{ {
@ -115,22 +115,8 @@ Item
{ {
tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura")
text: catalog.i18nc("@action:button", "Activate Configuration") text: catalog.i18nc("@action:button", "Activate Configuration")
visible: printerConnected && !isClusterPrinter() visible: false // printerConnected && !isClusterPrinter()
onClicked: manager.loadConfigurationFromPrinter() 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;
}
} }
} }

View 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

View file

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from . import NetworkPrinterOutputDevicePlugin
from . import DiscoverUM3Action from . import DiscoverUM3Action
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
from . import UM3OutputDevicePlugin
def getMetaData(): def getMetaData():
return {} return {}
def register(app): def register(app):
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()} return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}

View 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.

View file

@ -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. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.2
@ -34,44 +34,22 @@ UM.Dialog
} }
text: { text: {
if (manager.errorCode == 0) switch (manager.firmwareUpdateState)
{ {
if (manager.firmwareUpdateCompleteStatus) case 0:
{ return "" //Not doing anything (eg; idling)
//: Firmware update status label case 1:
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
return catalog.i18nc("@label","Updating firmware.") return catalog.i18nc("@label","Updating firmware.")
} case 2:
} return catalog.i18nc("@label","Firmware update completed.")
else case 3:
{ return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
switch (manager.errorCode) case 4:
{ return catalog.i18nc("@label","Firmware update failed due to an communication error.")
case 1: case 5:
//: Firmware update status label return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
return catalog.i18nc("@label","Firmware update failed due to an unknown error.") case 6:
case 2: return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
//: 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)
}
} }
} }
@ -81,16 +59,15 @@ UM.Dialog
ProgressBar ProgressBar
{ {
id: prog id: prog
value: manager.firmwareUpdateCompleteStatus ? 100 : manager.progress value: manager.firmwareProgress
minimumValue: 0 minimumValue: 0
maximumValue: 100 maximumValue: 100
indeterminate: (manager.progress < 1) && (!manager.firmwareUpdateCompleteStatus) indeterminate: manager.firmwareProgress < 1 && manager.firmwareProgress > 0
anchors anchors
{ {
left: parent.left; left: parent.left;
right: parent.right; right: parent.right;
} }
} }
SystemPalette SystemPalette

View 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

View file

@ -2,33 +2,32 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Signal import Signal, signalemitter from UM.Signal import Signal, signalemitter
from . import USBPrinterOutputDevice
from UM.Application import Application from UM.Application import Application
from UM.Resources import Resources from UM.Resources import Resources
from UM.Logger import Logger from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from cura.PrinterOutputDevice import ConnectionState from UM.i18n import i18nCatalog
from UM.Qt.ListModel import ListModel
from UM.Message import Message
from cura.PrinterOutputDevice import ConnectionState
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from . import USBPrinterOutputDevice
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
import threading import threading
import platform import platform
import time import time
import os.path
import serial.tools.list_ports 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") 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 @signalemitter
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
addUSBOutputDeviceSignal = Signal()
progressChanged = pyqtSignal()
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent = parent) super().__init__(parent = parent)
self._serial_port_list = [] self._serial_port_list = []
@ -38,39 +37,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
self._update_thread.setDaemon(True) self._update_thread.setDaemon(True)
self._check_updates = True self._check_updates = True
self._firmware_view = None
Application.getInstance().applicationShuttingDown.connect(self.stop) Application.getInstance().applicationShuttingDown.connect(self.stop)
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal. # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice)
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
def start(self): def start(self):
self._check_updates = True self._check_updates = True
@ -79,58 +49,28 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
def stop(self): def stop(self):
self._check_updates = False self._check_updates = False
def _updateThread(self): def _onConnectionStateChanged(self, serial_port):
while self._check_updates: if serial_port not in self._usb_output_devices:
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()
return return
for printer_connection in self._usb_output_devices: changed_device = self._usb_output_devices[serial_port]
self._usb_output_devices[printer_connection].resetFirmwareUpdate() if changed_device.connectionState == ConnectionState.connected:
self.spawnFirmwareInterface("") self.getOutputDeviceManager().addOutputDevice(changed_device)
for printer_connection in self._usb_output_devices: else:
try: self.getOutputDeviceManager().removeOutputDevice(serial_port)
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()
def _updateThread(self):
while self._check_updates:
container_stack = Application.getInstance().getGlobalContainerStack()
if container_stack is None:
time.sleep(5)
continue continue
if container_stack.getMetaDataEntry("supports_usb_connection"):
@pyqtSlot(str, str, result = bool) port_list = self.getSerialPortList(only_list_usb=True)
def updateFirmwareBySerial(self, serial_port, file_name): else:
if serial_port in self._usb_output_devices: port_list = [] # Just use an empty list; all USB devices will be removed.
self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort()) self._addRemovePorts(port_list)
try: time.sleep(5)
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
## Return the singleton instance of the USBPrinterManager ## Return the singleton instance of the USBPrinterManager
@classmethod @classmethod
@ -191,7 +131,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
Logger.log("w", "There is no firmware for machine %s.", machine_id) Logger.log("w", "There is no firmware for machine %s.", machine_id)
if hex_file: 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: else:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id) Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return "" return ""
@ -205,46 +149,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
continue continue
self._serial_port_list = list(serial_ports) self._serial_port_list = list(serial_ports)
devices_to_remove = []
for port, device in self._usb_output_devices.items(): for port, device in self._usb_output_devices.items():
if port not in self._serial_port_list: if port not in self._serial_port_list:
device.close() 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. ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addOutputDevice(self, serial_port): def addOutputDevice(self, serial_port):
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port) device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
device.connectionStateChanged.connect(self._onConnectionStateChanged) device.connectionStateChanged.connect(self._onConnectionStateChanged)
device.connect()
device.progressChanged.connect(self.progressChanged)
device.firmwareUpdateChange.connect(self.firmwareUpdateChange)
self._usb_output_devices[serial_port] = device self._usb_output_devices[serial_port] = device
device.connect()
## If one of the states of the connected devices change, we might need to add / remove them from the global list.
def _onConnectionStateChanged(self, serial_port):
success = True
try:
if self._usb_output_devices[serial_port].connectionState == ConnectionState.connected:
self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port])
else:
success = success and self.getOutputDeviceManager().removeOutputDevice(serial_port)
if success:
self.connectionStateChanged.emit()
except KeyError:
Logger.log("w", "Connection state of %s changed, but it was not found in the list")
@pyqtProperty(QObject , notify = connectionStateChanged)
def connectedPrinterList(self):
self._usb_output_devices_model = ListModel()
self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name")
self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer")
for connection in self._usb_output_devices:
if self._usb_output_devices[connection].connectionState == ConnectionState.connected:
self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]})
return self._usb_output_devices_model
## Create a list of serial ports on the system. ## Create a list of serial ports on the system.
# \param only_list_usb If true, only usb ports are listed # \param only_list_usb If true, only usb ports are listed

View file

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from . import USBPrinterOutputDeviceManager from . import USBPrinterOutputDeviceManager
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType from PyQt5.QtQml import qmlRegisterSingletonType
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {}
}
def register(app): def register(app):
# We are violating the QT API here (as we use a factory, which is technically not allowed). # 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-) # 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) 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()}

View file

@ -14,6 +14,9 @@ import Cura 1.0 as Cura
Cura.MachineAction Cura.MachineAction
{ {
anchors.fill: parent; anchors.fill: parent;
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
Item Item
{ {
id: upgradeFirmwareMachineAction id: upgradeFirmwareMachineAction
@ -60,16 +63,17 @@ Cura.MachineAction
{ {
id: autoUpgradeButton id: autoUpgradeButton
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware"); text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
enabled: parent.firmwareName != "" enabled: parent.firmwareName != "" && activeOutputDevice
onClicked: onClicked:
{ {
Cura.USBPrinterManager.updateAllFirmware(parent.firmwareName) activeOutputDevice.updateFirmware(parent.firmwareName)
} }
} }
Button Button
{ {
id: manualUpgradeButton id: manualUpgradeButton
text: catalog.i18nc("@action:button", "Upload custom Firmware"); text: catalog.i18nc("@action:button", "Upload custom Firmware");
enabled: activeOutputDevice != null
onClicked: onClicked:
{ {
customFirmwareDialog.open() customFirmwareDialog.open()
@ -83,7 +87,7 @@ Cura.MachineAction
title: catalog.i18nc("@title:window", "Select custom firmware") title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)" nameFilters: "Firmware image files (*.hex)"
selectExisting: true selectExisting: true
onAccepted: Cura.USBPrinterManager.updateAllFirmware(fileUrl) onAccepted: activeOutputDevice.updateFirmware(fileUrl)
} }
} }
} }

View file

@ -3,7 +3,6 @@
from . import BedLevelMachineAction from . import BedLevelMachineAction
from . import UpgradeFirmwareMachineAction from . import UpgradeFirmwareMachineAction
from . import UMOCheckupMachineAction
from . import UMOUpgradeSelection from . import UMOUpgradeSelection
from . import UM2UpgradeSelection from . import UM2UpgradeSelection
@ -18,7 +17,6 @@ def register(app):
return { "machine_action": [ return { "machine_action": [
BedLevelMachineAction.BedLevelMachineAction(), BedLevelMachineAction.BedLevelMachineAction(),
UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(), UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(),
UMOCheckupMachineAction.UMOCheckupMachineAction(),
UMOUpgradeSelection.UMOUpgradeSelection(), UMOUpgradeSelection.UMOUpgradeSelection(),
UM2UpgradeSelection.UM2UpgradeSelection() UM2UpgradeSelection.UM2UpgradeSelection()
]} ]}

View file

@ -47,9 +47,9 @@
"default_value": 30 "default_value": 30
}, },
"machine_start_gcode": { "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": { "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"
} }
} }
} }

View file

@ -40,10 +40,10 @@
"default_value": "RepRap" "default_value": "RepRap"
}, },
"machine_start_gcode": { "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": { "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}"
} }
} }
} }

View file

@ -1,55 +1,69 @@
{ {
"version":2, "version": 2,
"name":"Anycubic i3 Mega", "name": "Anycubic i3 Mega",
"inherits":"fdmprinter", "inherits": "fdmprinter",
"metadata":{ "metadata":
"visible":true, {
"author":"TheTobby", "visible": true,
"manufacturer":"Anycubic", "author": "TheTobby",
"file_formats":"text/x-gcode", "manufacturer": "Anycubic",
"icon":"icon_ultimaker2", "file_formats": "text/x-gcode",
"platform":"anycubic_i3_mega_platform.stl", "icon": "icon_ultimaker2",
"has_materials": false, "platform": "anycubic_i3_mega_platform.stl",
"has_machine_quality": true, "has_materials": false,
"preferred_quality": "*normal*" "has_machine_quality": true,
"preferred_quality": "*normal*"
}, },
"overrides":{ "overrides":
"machine_name":{ {
"default_value":"Anycubic i3 Mega" "machine_name":
{
"default_value": "Anycubic i3 Mega"
}, },
"machine_heated_bed":{ "machine_heated_bed":
"default_value":true {
"default_value": true
}, },
"machine_width":{ "machine_width":
"default_value":210 {
"default_value": 210
}, },
"machine_height":{ "machine_height":
"default_value":205 {
"default_value": 205
}, },
"machine_depth":{ "machine_depth":
"default_value":210 {
"default_value": 210
}, },
"machine_center_is_zero":{ "machine_center_is_zero":
"default_value":false {
"default_value": false
}, },
"machine_nozzle_size":{ "machine_nozzle_size":
"default_value":0.4 {
"default_value": 0.4
}, },
"material_diameter":{ "material_diameter":
"default_value":1.75 {
"default_value": 1.75
}, },
"gantry_height":{ "gantry_height":
"default_value":0 {
"default_value": 0
}, },
"machine_gcode_flavor":{ "machine_gcode_flavor":
"default_value":"RepRap (Marlin/Sprinter)" {
"default_value": "RepRap (Marlin/Sprinter)"
}, },
"machine_start_gcode":{ "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" {
"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":{ "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" {
"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"
} }
} }
} }

View file

@ -10,7 +10,7 @@
}, },
"overrides": { "overrides": {
"machine_start_gcode": { "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": { "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 --" "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 --"

View file

@ -3664,7 +3664,7 @@
"minimum_value": "0", "minimum_value": "0",
"maximum_value_warning": "100", "maximum_value_warning": "100",
"default_value": 15, "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", "enabled": "support_enable or support_tree_enable",
"limit_to_extruder": "support_infill_extruder_nr", "limit_to_extruder": "support_infill_extruder_nr",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -4264,6 +4264,18 @@
"limit_to_extruder": "support_infill_extruder_nr", "limit_to_extruder": "support_infill_extruder_nr",
"enabled": "support_enable and support_use_towers", "enabled": "support_enable and support_use_towers",
"settable_per_mesh": true "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
} }
} }
}, },
@ -5292,18 +5304,6 @@
"settable_per_meshgroup": false, "settable_per_meshgroup": false,
"settable_globally": 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": "anti_overhang_mesh":
{ {
"label": "Anti Overhang Mesh", "label": "Anti Overhang Mesh",

View file

@ -1,4 +1,5 @@
{ {
"id": "malyan_m180",
"version": 2, "version": 2,
"name": "Malyan M180", "name": "Malyan M180",
"inherits": "fdmprinter", "inherits": "fdmprinter",

View 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}
}
}

View 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" }
}
}

View 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" }
}
}

View file

@ -8,46 +8,59 @@
"manufacturer": "Tevo", "manufacturer": "Tevo",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"icon": "icon_ultimaker2", "icon": "icon_ultimaker2",
"has_materials": false, "has_materials": false,
"has_machine_quality": true, "has_machine_quality": true,
"platform": "prusai3_platform.stl", "platform": "tevo_blackwidow.stl",
"preferred_quality": "*normal*" "preferred_quality": "*normal*"
}, },
"overrides": { "overrides":
"machine_name": { {
"machine_name":
{
"default_value": "Tevo Black Widow" "default_value": "Tevo Black Widow"
}, },
"machine_heated_bed": { "machine_heated_bed":
{
"default_value": true "default_value": true
}, },
"machine_width": { "machine_width":
{
"default_value": 350 "default_value": 350
}, },
"machine_height": { "machine_height":
{
"default_value": 250 "default_value": 250
}, },
"machine_depth": { "machine_depth":
{
"default_value": 250 "default_value": 250
}, },
"machine_center_is_zero": { "machine_center_is_zero":
{
"default_value": false "default_value": false
}, },
"machine_nozzle_size": { "machine_nozzle_size":
{
"default_value": 0.4 "default_value": 0.4
}, },
"material_diameter": { "material_diameter":
"default_value": 1.75 {
"default_value": 1.75
}, },
"gantry_height": { "gantry_height":
{
"default_value": 0 "default_value": 0
}, },
"machine_gcode_flavor": { "machine_gcode_flavor":
{
"default_value": "RepRap (Marlin/Sprinter)" "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..." "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" "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"
} }
} }

Binary file not shown.

Binary file not shown.

View file

@ -396,6 +396,29 @@ UM.MainWindow
anchors.top: parent.top 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 Loader
{ {
id: sidebar id: sidebar
@ -456,29 +479,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 UM.MessageStack
{ {
anchors anchors

View file

@ -17,15 +17,39 @@ Item
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property real progress: printerConnected ? Cura.MachineManager.printerOutputDevices[0].progress : 0 property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null
property var activePrintJob: activePrinter ? activePrinter.activePrintJob: null
property real progress:
{
if(!printerConnected)
{
return 0
}
if(activePrinter == null)
{
return 0
}
if(activePrintJob == null)
{
return 0
}
if(activePrintJob.timeTotal == 0)
{
return 0 // Prevent devision by 0
}
return activePrintJob.timeElapsed / activePrintJob.timeTotal * 100
}
property int backendState: UM.Backend.state
property bool showProgress: { property bool showProgress: {
// determine if we need to show the progress bar + percentage // determine if we need to show the progress bar + percentage
if(!printerConnected || !printerAcceptsCommands) { if(activePrintJob == null)
{
return false; return false;
} }
switch(Cura.MachineManager.printerOutputDevices[0].jobState) switch(base.activePrintJob.state)
{ {
case "printing": case "printing":
case "paused": case "paused":
@ -46,18 +70,23 @@ Item
property variant statusColor: property variant statusColor:
{ {
if(!printerConnected || !printerAcceptsCommands) if(!printerConnected || !printerAcceptsCommands || activePrinter == null)
{
return UM.Theme.getColor("text"); return UM.Theme.getColor("text");
}
switch(Cura.MachineManager.printerOutputDevices[0].printerState) switch(activePrinter.state)
{ {
case "maintenance": case "maintenance":
return UM.Theme.getColor("status_busy"); return UM.Theme.getColor("status_busy");
case "error": case "error":
return UM.Theme.getColor("status_stopped"); return UM.Theme.getColor("status_stopped");
} }
if(base.activePrintJob == null)
switch(Cura.MachineManager.printerOutputDevices[0].jobState) {
return UM.Theme.getColor("text");
}
switch(base.activePrintJob.state)
{ {
case "printing": case "printing":
case "pre_print": case "pre_print":
@ -84,17 +113,30 @@ Item
property string statusText: property string statusText:
{ {
if(!printerConnected) if(!printerConnected)
{
return catalog.i18nc("@label:MonitorStatus", "Not connected to a printer"); return catalog.i18nc("@label:MonitorStatus", "Not connected to a printer");
}
if(!printerAcceptsCommands) if(!printerAcceptsCommands)
{
return catalog.i18nc("@label:MonitorStatus", "Printer does not accept commands"); return catalog.i18nc("@label:MonitorStatus", "Printer does not accept commands");
}
var printerOutputDevice = Cura.MachineManager.printerOutputDevices[0] var printerOutputDevice = Cura.MachineManager.printerOutputDevices[0]
if(activePrinter == null)
if(printerOutputDevice.printerState == "maintenance") {
return "";
}
if(activePrinter.state == "maintenance")
{ {
return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer"); return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer");
} }
switch(printerOutputDevice.jobState)
if(base.activePrintJob == null)
{
return " "
}
switch(base.activePrintJob.state)
{ {
case "offline": case "offline":
return catalog.i18nc("@label:MonitorStatus", "Lost connection with the printer"); return catalog.i18nc("@label:MonitorStatus", "Lost connection with the printer");
@ -162,7 +204,11 @@ Item
{ {
return false; return false;
} }
switch(Cura.MachineManager.printerOutputDevices[0].jobState) if(base.activePrintJob == null)
{
return false
}
switch(base.activePrintJob.state)
{ {
case "pausing": case "pausing":
case "resuming": case "resuming":
@ -184,7 +230,8 @@ Item
anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width; anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width;
} }
Row { Row
{
id: buttonsRow id: buttonsRow
height: abortButton.height height: abortButton.height
anchors.top: progressBar.bottom anchors.top: progressBar.bottom
@ -193,7 +240,8 @@ Item
anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width
spacing: UM.Theme.getSize("default_margin").width spacing: UM.Theme.getSize("default_margin").width
Row { Row
{
id: additionalComponentsRow id: additionalComponentsRow
spacing: UM.Theme.getSize("default_margin").width spacing: UM.Theme.getSize("default_margin").width
} }
@ -224,19 +272,17 @@ Item
property bool userClicked: false property bool userClicked: false
property string lastJobState: "" property string lastJobState: ""
visible: printerConnected && Cura.MachineManager.printerOutputDevices[0].canPause visible: printerConnected && activePrinter != null &&activePrinter.canPause
enabled: (!userClicked) && printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands && enabled: (!userClicked) && printerConnected && printerAcceptsCommands && activePrintJob != null &&
(["paused", "printing"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0) (["paused", "printing"].indexOf(activePrintJob.state) >= 0)
text: { text: {
var result = ""; if (!printerConnected || activePrintJob == null)
if (!printerConnected)
{ {
return ""; return catalog.i18nc("@label:", "Pause");
} }
var jobState = Cura.MachineManager.printerOutputDevices[0].jobState;
if (jobState == "paused") if (activePrintJob.state == "paused")
{ {
return catalog.i18nc("@label:", "Resume"); return catalog.i18nc("@label:", "Resume");
} }
@ -247,14 +293,17 @@ Item
} }
onClicked: onClicked:
{ {
var current_job_state = Cura.MachineManager.printerOutputDevices[0].jobState if(activePrintJob == null)
if(current_job_state == "paused")
{ {
Cura.MachineManager.printerOutputDevices[0].setJobState("print"); return // Do nothing!
} }
else if(current_job_state == "printing") if(activePrintJob.state == "paused")
{ {
Cura.MachineManager.printerOutputDevices[0].setJobState("pause"); activePrintJob.setState("print");
}
else if(activePrintJob.state == "printing")
{
activePrintJob.setState("pause");
} }
} }
@ -265,9 +314,9 @@ Item
{ {
id: abortButton id: abortButton
visible: printerConnected && Cura.MachineManager.printerOutputDevices[0].canAbort visible: printerConnected && activePrinter != null && activePrinter.canAbort
enabled: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands && enabled: printerConnected && printerAcceptsCommands && activePrintJob != null &&
(["paused", "printing", "pre_print"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0) (["paused", "printing", "pre_print"].indexOf(activePrintJob.state) >= 0)
height: UM.Theme.getSize("save_button_save_to_button").height height: UM.Theme.getSize("save_button_save_to_button").height
@ -286,7 +335,7 @@ Item
text: catalog.i18nc("@label", "Are you sure you want to abort the print?") text: catalog.i18nc("@label", "Are you sure you want to abort the print?")
standardButtons: StandardButton.Yes | StandardButton.No standardButtons: StandardButton.Yes | StandardButton.No
Component.onCompleted: visible = false Component.onCompleted: visible = false
onYes: Cura.MachineManager.printerOutputDevices[0].setJobState("abort") onYes: activePrintJob.setState("abort")
} }
} }
} }

View file

@ -143,7 +143,7 @@ UM.ManagementPage
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property var connectedPrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null property var connectedPrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property var printJob: connectedPrinter != null ? connectedPrinter.activePrintJob: null
Label Label
{ {
text: catalog.i18nc("@label", "Printer type:") text: catalog.i18nc("@label", "Printer type:")
@ -178,7 +178,12 @@ UM.ManagementPage
return ""; return "";
} }
switch(Cura.MachineManager.printerOutputDevices[0].jobState) if (machineInfo.printJob == null)
{
return catalog.i18nc("@label:MonitorStatus", "Waiting for a printjob");
}
switch(machineInfo.printJob.state)
{ {
case "printing": case "printing":
return catalog.i18nc("@label:MonitorStatus", "Printing..."); return catalog.i18nc("@label:MonitorStatus", "Printing...");
@ -194,10 +199,9 @@ UM.ManagementPage
return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer"); return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer");
case "abort": // note sure if this jobState actually occurs in the wild case "abort": // note sure if this jobState actually occurs in the wild
return catalog.i18nc("@label:MonitorStatus", "Aborting print..."); return catalog.i18nc("@label:MonitorStatus", "Aborting print...");
case "ready": // ready to print or getting ready
case "": // ready to print or getting ready
return catalog.i18nc("@label:MonitorStatus", "Waiting for a printjob");
} }
return ""
} }
visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId && machineInfo.printerAcceptsCommands visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId && machineInfo.printerAcceptsCommands
wrapMode: Text.WordWrap wrapMode: Text.WordWrap

View file

@ -72,7 +72,7 @@ TabView
width: scrollView.columnWidth; width: scrollView.columnWidth;
text: properties.name; text: properties.name;
readOnly: !base.editingEnabled; readOnly: !base.editingEnabled;
onEditingFinished: base.setName(properties.name, text) onEditingFinished: base.updateMaterialDisplayName(properties.name, text)
} }
Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Brand") } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Brand") }
@ -82,11 +82,7 @@ TabView
width: scrollView.columnWidth; width: scrollView.columnWidth;
text: properties.supplier; text: properties.supplier;
readOnly: !base.editingEnabled; readOnly: !base.editingEnabled;
onEditingFinished: onEditingFinished: base.updateMaterialSupplier(properties.supplier, text)
{
base.setMetaDataEntry("brand", properties.supplier, text);
pane.objectList.currentIndex = pane.getIndexById(base.containerId);
}
} }
Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Material Type") } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Material Type") }
@ -95,23 +91,17 @@ TabView
width: scrollView.columnWidth; width: scrollView.columnWidth;
text: properties.material_type; text: properties.material_type;
readOnly: !base.editingEnabled; readOnly: !base.editingEnabled;
onEditingFinished: onEditingFinished: base.updateMaterialType(properties.material_type, text)
{
base.setMetaDataEntry("material", properties.material_type, text);
pane.objectList.currentIndex = pane.getIndexById(base.containerId)
}
} }
Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") }
Row {
Row width: scrollView.columnWidth
{ height: parent.rowHeight
width: scrollView.columnWidth;
height: parent.rowHeight;
spacing: Math.floor(UM.Theme.getSize("default_margin").width/2) spacing: Math.floor(UM.Theme.getSize("default_margin").width/2)
Rectangle // color indicator square
{ Rectangle {
id: colorSelector id: colorSelector
color: properties.color_code color: properties.color_code
@ -121,17 +111,29 @@ TabView
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea { anchors.fill: parent; onClicked: colorDialog.open(); enabled: base.editingEnabled } // open the color selection dialog on click
MouseArea {
anchors.fill: parent
onClicked: colorDialog.open()
enabled: base.editingEnabled
}
} }
ReadOnlyTextField
{ // pretty color name text field
ReadOnlyTextField {
id: colorLabel; id: colorLabel;
text: properties.color_name; text: properties.color_name;
readOnly: !base.editingEnabled readOnly: !base.editingEnabled
onEditingFinished: base.setMetaDataEntry("color_name", properties.color_name, text) onEditingFinished: base.setMetaDataEntry("color_name", properties.color_name, text)
} }
ColorDialog { id: colorDialog; color: properties.color_code; onAccepted: base.setMetaDataEntry("color_code", properties.color_code, color) } // popup dialog to select a new color
// if successful it sets the properties.color_code value to the new color
ColorDialog {
id: colorDialog
color: properties.color_code
onAccepted: base.setMetaDataEntry("color_code", properties.color_code, color)
}
} }
Item { width: parent.width; height: UM.Theme.getSize("default_margin").height } Item { width: parent.width; height: UM.Theme.getSize("default_margin").height }
@ -401,11 +403,11 @@ TabView
} }
// Tiny convenience function to check if a value really changed before trying to set it. // Tiny convenience function to check if a value really changed before trying to set it.
function setMetaDataEntry(entry_name, old_value, new_value) function setMetaDataEntry(entry_name, old_value, new_value) {
{ if (old_value != new_value) {
if(old_value != new_value) Cura.ContainerManager.setContainerMetaDataEntry(base.containerId, entry_name, new_value)
{ // make sure the UI properties are updated as well since we don't re-fetch the entire model here
Cura.ContainerManager.setContainerMetaDataEntry(base.containerId, entry_name, new_value); properties[entry_name] = new_value
} }
} }
@ -435,14 +437,28 @@ TabView
return 0; return 0;
} }
function setName(old_value, new_value) // update the display name of the material
{ function updateMaterialDisplayName (old_name, new_name) {
if(old_value != new_value)
{ // don't change when new name is the same
Cura.ContainerManager.setContainerName(base.containerId, new_value); if (old_name == new_name) {
// update material name label. not so pretty, but it works return
materialProperties.name = new_value;
pane.objectList.currentIndex = pane.getIndexById(base.containerId)
} }
// update the values
Cura.ContainerManager.setContainerName(base.containerId, new_name)
materialProperties.name = new_name
}
// update the type of the material
function updateMaterialType (old_type, new_type) {
base.setMetaDataEntry("material", old_type, new_type)
materialProperties.material_type = new_type
}
// update the supplier of the material
function updateMaterialSupplier (old_supplier, new_supplier) {
base.setMetaDataEntry("brand", old_supplier, new_supplier)
materialProperties.supplier = new_supplier
} }
} }

View file

@ -132,93 +132,73 @@ UM.ManagementPage
} }
buttons: [ buttons: [
Button
{ // Activate button
text: catalog.i18nc("@action:button", "Activate"); Button {
text: catalog.i18nc("@action:button", "Activate")
iconName: "list-activate"; iconName: "list-activate";
enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId && Cura.MachineManager.hasMaterials enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId && Cura.MachineManager.hasMaterials
onClicked: onClicked: {
{ forceActiveFocus()
forceActiveFocus();
Cura.MachineManager.setActiveMaterial(base.currentItem.id) Cura.MachineManager.setActiveMaterial(base.currentItem.id)
currentItem = base.model.getItem(base.objectList.currentIndex) // Refresh the current item. currentItem = base.model.getItem(base.objectList.currentIndex) // Refresh the current item.
} }
}, },
Button
{ // Create button
Button {
text: catalog.i18nc("@action:button", "Create") text: catalog.i18nc("@action:button", "Create")
iconName: "list-add" iconName: "list-add"
onClicked: onClicked: {
{ forceActiveFocus()
forceActiveFocus(); Cura.ContainerManager.createMaterial()
var material_id = Cura.ContainerManager.createMaterial()
if(material_id == "")
{
return
}
if(Cura.MachineManager.hasMaterials)
{
Cura.MachineManager.setActiveMaterial(material_id)
}
base.objectList.currentIndex = base.getIndexById(material_id);
} }
}, },
Button
{ // Duplicate button
Button {
text: catalog.i18nc("@action:button", "Duplicate"); text: catalog.i18nc("@action:button", "Duplicate");
iconName: "list-add"; iconName: "list-add";
enabled: base.currentItem != null enabled: base.currentItem != null
onClicked: onClicked: {
{ forceActiveFocus()
forceActiveFocus(); Cura.ContainerManager.duplicateOriginalMaterial(base.currentItem.id)
var base_file = Cura.ContainerManager.getContainerMetaDataEntry(base.currentItem.id, "base_file")
// We need to copy the base container instead of the specific variant.
var material_id = base_file == "" ? Cura.ContainerManager.duplicateMaterial(base.currentItem.id): Cura.ContainerManager.duplicateMaterial(base_file)
if(material_id == "")
{
return
}
if(Cura.MachineManager.hasMaterials)
{
Cura.MachineManager.setActiveMaterial(material_id)
}
// TODO: this doesn't work because the source is a bit delayed
base.objectList.currentIndex = base.getIndexById(material_id);
} }
}, },
Button
{ // Remove button
text: catalog.i18nc("@action:button", "Remove"); Button {
iconName: "list-remove"; text: catalog.i18nc("@action:button", "Remove")
iconName: "list-remove"
enabled: base.currentItem != null && !base.currentItem.readOnly && !Cura.ContainerManager.isContainerUsed(base.currentItem.id) enabled: base.currentItem != null && !base.currentItem.readOnly && !Cura.ContainerManager.isContainerUsed(base.currentItem.id)
onClicked: onClicked: {
{ forceActiveFocus()
forceActiveFocus(); confirmDialog.open()
confirmDialog.open();
} }
}, },
Button
{ // Import button
text: catalog.i18nc("@action:button", "Import"); Button {
iconName: "document-import"; text: catalog.i18nc("@action:button", "Import")
onClicked: iconName: "document-import"
{ onClicked: {
forceActiveFocus(); forceActiveFocus()
importDialog.open(); importDialog.open()
} }
visible: true; visible: true
}, },
Button
{ // Export button
Button {
text: catalog.i18nc("@action:button", "Export") text: catalog.i18nc("@action:button", "Export")
iconName: "document-export" iconName: "document-export"
onClicked: onClicked: {
{ forceActiveFocus()
forceActiveFocus(); exportDialog.open()
exportDialog.open();
} }
enabled: currentItem != null enabled: currentItem != null
} }
] ]
Item { Item {

View file

@ -213,8 +213,8 @@ UM.ManagementPage
ProfileTab ProfileTab
{ {
title: catalog.i18nc("@title:tab", "Global Settings"); title: catalog.i18nc("@title:tab", "Global Settings");
quality: base.currentItem != null ? base.currentItem.id : ""; quality: Cura.MachineManager.activeMachine.qualityChanges.id
material: Cura.MachineManager.allActiveMaterialIds[Cura.MachineManager.activeMachineId] material: Cura.MachineManager.activeMachine.material.id
} }
Repeater Repeater

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,201 @@
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Item
{
property alias color: background.color
property var extruderModel
property var position: index
//width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2)
implicitWidth: parent.width
implicitHeight: UM.Theme.getSize("sidebar_extruder_box").height
Rectangle
{
id: background
anchors.fill: parent
Label //Extruder name.
{
text: Cura.ExtruderManager.getExtruderName(position) != "" ? Cura.ExtruderManager.getExtruderName(position) : catalog.i18nc("@label", "Extruder")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: UM.Theme.getSize("default_margin").width
}
Label //Target temperature.
{
id: extruderTargetTemperature
text: Math.round(extruderModel.targetHotendTemperature) + "°C"
//text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.targetHotendTemperatures[index] != null) ? Math.round(connectedPrinter.targetHotendTemperatures[index]) + "°C" : ""
font: UM.Theme.getFont("small")
color: UM.Theme.getColor("text_inactive")
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.bottom: extruderTemperature.bottom
MouseArea //For tooltip.
{
id: extruderTargetTemperatureTooltipArea
hoverEnabled: true
anchors.fill: parent
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: extruderTargetTemperature.mapToItem(base, 0, -parent.height / 4).y},
catalog.i18nc("@tooltip", "The target temperature of the hotend. The hotend will heat up or cool down towards this temperature. If this is 0, the hotend heating is turned off.")
);
}
else
{
base.hideTooltip();
}
}
}
}
Label //Temperature indication.
{
id: extruderTemperature
text: Math.round(extruderModel.hotendTemperature) + "°C"
//text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.hotendTemperatures[index] != null) ? Math.round(connectedPrinter.hotendTemperatures[index]) + "°C" : ""
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("large")
anchors.right: extruderTargetTemperature.left
anchors.top: parent.top
anchors.margins: UM.Theme.getSize("default_margin").width
MouseArea //For tooltip.
{
id: extruderTemperatureTooltipArea
hoverEnabled: true
anchors.fill: parent
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y},
catalog.i18nc("@tooltip", "The current temperature of this extruder.")
);
}
else
{
base.hideTooltip();
}
}
}
}
Rectangle //Material colour indication.
{
id: materialColor
width: Math.floor(materialName.height * 0.75)
height: Math.floor(materialName.height * 0.75)
radius: width / 2
color: extruderModel.activeMaterial ? extruderModel.activeMaterial.color: "#00000000"
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: extruderModel.activeMaterial != null
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: materialName.verticalCenter
MouseArea //For tooltip.
{
id: materialColorTooltipArea
hoverEnabled: true
anchors.fill: parent
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: parent.mapToItem(base, 0, -parent.height / 2).y},
catalog.i18nc("@tooltip", "The colour of the material in this extruder.")
);
}
else
{
base.hideTooltip();
}
}
}
}
Label //Material name.
{
id: materialName
text: extruderModel.activeMaterial != null ? extruderModel.activeMaterial.type : ""
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
anchors.left: materialColor.right
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").width
MouseArea //For tooltip.
{
id: materialNameTooltipArea
hoverEnabled: true
anchors.fill: parent
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: parent.mapToItem(base, 0, 0).y},
catalog.i18nc("@tooltip", "The material in this extruder.")
);
}
else
{
base.hideTooltip();
}
}
}
}
Label //Variant name.
{
id: variantName
text: extruderModel.hotendID
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").width
MouseArea //For tooltip.
{
id: variantNameTooltipArea
hoverEnabled: true
anchors.fill: parent
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y},
catalog.i18nc("@tooltip", "The nozzle inserted in this extruder.")
);
}
else
{
base.hideTooltip();
}
}
}
}
}
}

View file

@ -0,0 +1,352 @@
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Item
{
implicitWidth: parent.width
height: visible ? UM.Theme.getSize("sidebar_extruder_box").height : 0
property var printerModel
Rectangle
{
color: UM.Theme.getColor("sidebar")
anchors.fill: parent
Label //Build plate label.
{
text: catalog.i18nc("@label", "Build plate")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: UM.Theme.getSize("default_margin").width
}
Label //Target temperature.
{
id: bedTargetTemperature
text: printerModel != null ? printerModel.targetBedTemperature + "°C" : ""
font: UM.Theme.getFont("small")
color: UM.Theme.getColor("text_inactive")
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.bottom: bedCurrentTemperature.bottom
MouseArea //For tooltip.
{
id: bedTargetTemperatureTooltipArea
hoverEnabled: true
anchors.fill: parent
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: bedTargetTemperature.mapToItem(base, 0, -parent.height / 4).y},
catalog.i18nc("@tooltip", "The target temperature of the heated bed. The bed will heat up or cool down towards this temperature. If this is 0, the bed heating is turned off.")
);
}
else
{
base.hideTooltip();
}
}
}
}
Label //Current temperature.
{
id: bedCurrentTemperature
text: printerModel != null ? printerModel.bedTemperature + "°C" : ""
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
anchors.right: bedTargetTemperature.left
anchors.top: parent.top
anchors.margins: UM.Theme.getSize("default_margin").width
MouseArea //For tooltip.
{
id: bedTemperatureTooltipArea
hoverEnabled: true
anchors.fill: parent
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: bedCurrentTemperature.mapToItem(base, 0, -parent.height / 4).y},
catalog.i18nc("@tooltip", "The current temperature of the heated bed.")
);
}
else
{
base.hideTooltip();
}
}
}
}
Rectangle //Input field for pre-heat temperature.
{
id: preheatTemperatureControl
color: !enabled ? UM.Theme.getColor("setting_control_disabled") : showError ? UM.Theme.getColor("setting_validation_error_background") : UM.Theme.getColor("setting_validation_ok")
property var showError:
{
if(bedTemperature.properties.maximum_value != "None" && bedTemperature.properties.maximum_value < Math.floor(preheatTemperatureInput.text))
{
return true;
} else
{
return false;
}
}
enabled:
{
if (printerModel == null)
{
return false; //Can't preheat if not connected.
}
if (!connectedPrinter.acceptsCommands)
{
return false; //Not allowed to do anything.
}
if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline")
{
return false; //Printer is in a state where it can't react to pre-heating.
}
return true;
}
border.width: UM.Theme.getSize("default_lining").width
border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border")
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
width: UM.Theme.getSize("setting_control").width
height: UM.Theme.getSize("setting_control").height
visible: printerModel != null ? printerModel.canPreHeatBed: true
Rectangle //Highlight of input field.
{
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_lining").width
color: UM.Theme.getColor("setting_control_highlight")
opacity: preheatTemperatureControl.hovered ? 1.0 : 0
}
MouseArea //Change cursor on hovering.
{
id: preheatTemperatureInputMouseArea
hoverEnabled: true
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onHoveredChanged:
{
if (containsMouse)
{
base.showTooltip(
base,
{x: 0, y: preheatTemperatureInputMouseArea.mapToItem(base, 0, 0).y},
catalog.i18nc("@tooltip of temperature input", "The temperature to pre-heat the bed to.")
);
}
else
{
base.hideTooltip();
}
}
}
TextInput
{
id: preheatTemperatureInput
font: UM.Theme.getFont("default")
color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text")
selectByMouse: true
maximumLength: 10
enabled: parent.enabled
validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex.
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
renderType: Text.NativeRendering
Component.onCompleted:
{
if (!bedTemperature.properties.value)
{
text = "";
}
if ((bedTemperature.resolve != "None" && bedTemperature.resolve) && (bedTemperature.stackLevels[0] != 0) && (bedTemperature.stackLevels[0] != 1))
{
// We have a resolve function. Indicates that the setting is not settable per extruder and that
// we have to choose between the resolved value (default) and the global value
// (if user has explicitly set this).
text = bedTemperature.resolve;
}
else
{
text = bedTemperature.properties.value;
}
}
}
}
Button //The pre-heat button.
{
id: preheatButton
height: UM.Theme.getSize("setting_control").height
visible: printerModel != null ? printerModel.canPreHeatBed: true
enabled:
{
if (!preheatTemperatureControl.enabled)
{
return false; //Not connected, not authenticated or printer is busy.
}
if (printerModel.isPreheating)
{
return true;
}
if (bedTemperature.properties.minimum_value != "None" && Math.floor(preheatTemperatureInput.text) < Math.floor(bedTemperature.properties.minimum_value))
{
return false; //Target temperature too low.
}
if (bedTemperature.properties.maximum_value != "None" && Math.floor(preheatTemperatureInput.text) > Math.floor(bedTemperature.properties.maximum_value))
{
return false; //Target temperature too high.
}
if (Math.floor(preheatTemperatureInput.text) == 0)
{
return false; //Setting the temperature to 0 is not allowed (since that cancels the pre-heating).
}
return true; //Preconditions are met.
}
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").width
style: ButtonStyle {
background: Rectangle
{
border.width: UM.Theme.getSize("default_lining").width
implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("default_margin").width * 2)
border.color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled_border");
}
else if(control.pressed)
{
return UM.Theme.getColor("action_button_active_border");
}
else if(control.hovered)
{
return UM.Theme.getColor("action_button_hovered_border");
}
else
{
return UM.Theme.getColor("action_button_border");
}
}
color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled");
}
else if(control.pressed)
{
return UM.Theme.getColor("action_button_active");
}
else if(control.hovered)
{
return UM.Theme.getColor("action_button_hovered");
}
else
{
return UM.Theme.getColor("action_button");
}
}
Behavior on color
{
ColorAnimation
{
duration: 50
}
}
Label
{
id: actualLabel
anchors.centerIn: parent
color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled_text");
}
else if(control.pressed)
{
return UM.Theme.getColor("action_button_active_text");
}
else if(control.hovered)
{
return UM.Theme.getColor("action_button_hovered_text");
}
else
{
return UM.Theme.getColor("action_button_text");
}
}
font: UM.Theme.getFont("action_button")
text:
{
if(printerModel == null)
{
return ""
}
if(printerModel.isPreheating )
{
return catalog.i18nc("@button Cancel pre-heating", "Cancel")
} else
{
return catalog.i18nc("@button", "Pre-heat")
}
}
}
}
}
onClicked:
{
if (!printerModel.isPreheating)
{
printerModel.preheatBed(preheatTemperatureInput.text, 900);
}
else
{
printerModel.cancelPreheatBed();
}
}
onHoveredChanged:
{
if (hovered)
{
base.showTooltip(
base,
{x: 0, y: preheatButton.mapToItem(base, 0, 0).y},
catalog.i18nc("@tooltip of pre-heat", "Heat the bed in advance before printing. You can continue adjusting your print while it is heating, and you won't have to wait for the bed to heat up when you're ready to print.")
);
}
else
{
base.hideTooltip();
}
}
}
}
}

View file

@ -0,0 +1,442 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Item
{
property var printerModel
property var activePrintJob: printerModel != null ? printerModel.activePrintJob : null
implicitWidth: parent.width
implicitHeight: childrenRect.height
Component
{
id: monitorButtonStyle
ButtonStyle
{
background: Rectangle
{
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled_border");
}
else if(control.pressed)
{
return UM.Theme.getColor("action_button_active_border");
}
else if(control.hovered)
{
return UM.Theme.getColor("action_button_hovered_border");
}
return UM.Theme.getColor("action_button_border");
}
color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled");
}
else if(control.pressed)
{
return UM.Theme.getColor("action_button_active");
}
else if(control.hovered)
{
return UM.Theme.getColor("action_button_hovered");
}
return UM.Theme.getColor("action_button");
}
Behavior on color
{
ColorAnimation
{
duration: 50
}
}
}
label: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2)
height: Math.floor(control.height / 2)
sourceSize.width: width
sourceSize.height: width
color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled_text");
}
else if(control.pressed)
{
return UM.Theme.getColor("action_button_active_text");
}
else if(control.hovered)
{
return UM.Theme.getColor("action_button_hovered_text");
}
return UM.Theme.getColor("action_button_text");
}
source: control.iconSource
}
}
}
}
Column
{
enabled:
{
if (printerModel == null)
{
return false; //Can't control the printer if not connected
}
if (!connectedDevice.acceptsCommands)
{
return false; //Not allowed to do anything.
}
if(activePrintJob == null)
{
return true
}
if (activePrintJob.state == "printing" || activePrintJob.state == "resuming" || activePrintJob.state == "pausing" || activePrintJob.state == "error" || activePrintJob.state == "offline")
{
return false; //Printer is in a state where it can't react to manual control
}
return true;
}
MonitorSection
{
label: catalog.i18nc("@label", "Printer control")
width: base.width
}
Row
{
width: base.width - 2 * UM.Theme.getSize("default_margin").width
height: childrenRect.height + UM.Theme.getSize("default_margin").width
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("default_margin").width
Label
{
text: catalog.i18nc("@label", "Jog Position")
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
width: Math.floor(parent.width * 0.4) - UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("setting_control").height
verticalAlignment: Text.AlignVCenter
}
GridLayout
{
columns: 3
rows: 4
rowSpacing: UM.Theme.getSize("default_lining").width
columnSpacing: UM.Theme.getSize("default_lining").height
Label
{
text: catalog.i18nc("@label", "X/Y")
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
width: height
height: UM.Theme.getSize("setting_control").height
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
Layout.row: 1
Layout.column: 2
Layout.preferredWidth: width
Layout.preferredHeight: height
}
Button
{
Layout.row: 2
Layout.column: 2
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_top");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.moveHead(0, distancesRow.currentDistance, 0)
}
}
Button
{
Layout.row: 3
Layout.column: 1
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_left");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.moveHead(-distancesRow.currentDistance, 0, 0)
}
}
Button
{
Layout.row: 3
Layout.column: 3
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_right");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.moveHead(distancesRow.currentDistance, 0, 0)
}
}
Button
{
Layout.row: 4
Layout.column: 2
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_bottom");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.moveHead(0, -distancesRow.currentDistance, 0)
}
}
Button
{
Layout.row: 3
Layout.column: 2
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("home");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.homeHead()
}
}
}
Column
{
spacing: UM.Theme.getSize("default_lining").height
Label
{
text: catalog.i18nc("@label", "Z")
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
width: UM.Theme.getSize("section").height
height: UM.Theme.getSize("setting_control").height
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
Button
{
iconSource: UM.Theme.getIcon("arrow_top");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.moveHead(0, 0, distancesRow.currentDistance)
}
}
Button
{
iconSource: UM.Theme.getIcon("home");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.homeBed()
}
}
Button
{
iconSource: UM.Theme.getIcon("arrow_bottom");
style: monitorButtonStyle
width: height
height: UM.Theme.getSize("setting_control").height
onClicked:
{
printerModel.moveHead(0, 0, -distancesRow.currentDistance)
}
}
}
}
Row
{
id: distancesRow
width: base.width - 2 * UM.Theme.getSize("default_margin").width
height: childrenRect.height + UM.Theme.getSize("default_margin").width
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("default_margin").width
property real currentDistance: 10
Label
{
text: catalog.i18nc("@label", "Jog Distance")
color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default")
width: Math.floor(parent.width * 0.4) - UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("setting_control").height
verticalAlignment: Text.AlignVCenter
}
Row
{
Repeater
{
model: distancesModel
delegate: Button
{
height: UM.Theme.getSize("setting_control").height
width: height + UM.Theme.getSize("default_margin").width
text: model.label
exclusiveGroup: distanceGroup
checkable: true
checked: distancesRow.currentDistance == model.value
onClicked: distancesRow.currentDistance = model.value
style: ButtonStyle {
background: Rectangle {
border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width
border.color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled_border");
}
else if (control.checked || control.pressed)
{
return UM.Theme.getColor("action_button_active_border");
}
else if(control.hovered)
{
return UM.Theme.getColor("action_button_hovered_border");
}
return UM.Theme.getColor("action_button_border");
}
color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled");
}
else if (control.checked || control.pressed)
{
return UM.Theme.getColor("action_button_active");
}
else if (control.hovered)
{
return UM.Theme.getColor("action_button_hovered");
}
return UM.Theme.getColor("action_button");
}
Behavior on color { ColorAnimation { duration: 50; } }
Label {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: UM.Theme.getSize("default_lining").width * 2
anchors.rightMargin: UM.Theme.getSize("default_lining").width * 2
color:
{
if(!control.enabled)
{
return UM.Theme.getColor("action_button_disabled_text");
}
else if (control.checked || control.pressed)
{
return UM.Theme.getColor("action_button_active_text");
}
else if (control.hovered)
{
return UM.Theme.getColor("action_button_hovered_text");
}
return UM.Theme.getColor("action_button_text");
}
font: UM.Theme.getFont("default")
text: control.text
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideMiddle
}
}
label: Item { }
}
}
}
}
}
ListModel
{
id: distancesModel
ListElement { label: "0.1"; value: 0.1 }
ListElement { label: "1"; value: 1 }
ListElement { label: "10"; value: 10 }
ListElement { label: "100"; value: 100 }
}
ExclusiveGroup { id: distanceGroup }
}
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Item
{
property string label: ""
property string value: ""
height: childrenRect.height;
Row
{
height: UM.Theme.getSize("setting_control").height
width: Math.floor(base.width - 2 * UM.Theme.getSize("default_margin").width)
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
Label
{
width: Math.floor(parent.width * 0.4)
anchors.verticalCenter: parent.verticalCenter
text: label
color: connectedPrinter != null && connectedPrinter.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text")
font: UM.Theme.getFont("default")
elide: Text.ElideRight
}
Label
{
width: Math.floor(parent.width * 0.6)
anchors.verticalCenter: parent.verticalCenter
text: value
color: connectedPrinter != null && connectedPrinter.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text")
font: UM.Theme.getFont("default")
elide: Text.ElideRight
}
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Item
{
id: base
property string label
height: childrenRect.height;
Rectangle
{
color: UM.Theme.getColor("setting_category")
width: base.width
height: UM.Theme.getSize("section").height
Label
{
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
text: label
font: UM.Theme.getFont("setting_category")
color: UM.Theme.getColor("setting_category_text")
}
}
}

View file

@ -0,0 +1,59 @@
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Item
{
implicitWidth: parent.width
implicitHeight: Math.floor(childrenRect.height + UM.Theme.getSize("default_margin").height * 2)
property var outputDevice: null
Rectangle
{
anchors.fill: parent
color: UM.Theme.getColor("setting_category")
property var activePrinter: outputDevice != null ? outputDevice.activePrinter : null
Label
{
id: outputDeviceNameLabel
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: UM.Theme.getSize("default_margin").width
text: outputDevice != null ? activePrinter.name : ""
}
Label
{
id: outputDeviceAddressLabel
text: (outputDevice != null && outputDevice.address != null) ? outputDevice.address : ""
font: UM.Theme.getFont("small")
color: UM.Theme.getColor("text_inactive")
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
}
Label
{
text: outputDevice != null ? "" : catalog.i18nc("@info:status", "The printer is not connected.")
color: outputDevice != null && outputDevice.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text")
font: UM.Theme.getFont("very_small")
wrapMode: Text.WordWrap
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
}
}
}

View file

@ -170,8 +170,8 @@ Item {
tooltip: [1, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@info:tooltip","Slice current printjob") : catalog.i18nc("@info:tooltip","Cancel slicing process") tooltip: [1, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@info:tooltip","Slice current printjob") : catalog.i18nc("@info:tooltip","Cancel slicing process")
// 1 = not started, 2 = Processing // 1 = not started, 2 = Processing
enabled: base.backendState != "undefined" && (base.backendState == 1 || base.backendState == 2) && base.activity == true enabled: base.backendState != "undefined" && ([1, 2].indexOf(base.backendState) != -1) && base.activity
visible: base.backendState != "undefined" && !autoSlice && (base.backendState == 1 || base.backendState == 2) && base.activity == true visible: base.backendState != "undefined" && !autoSlice && ([1, 2, 4].indexOf(base.backendState) != -1) && base.activity
property bool autoSlice property bool autoSlice
height: UM.Theme.getSize("save_button_save_to_button").height height: UM.Theme.getSize("save_button_save_to_button").height
@ -179,8 +179,8 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width
// 1 = not started, 5 = disabled // 1 = not started, 4 = error, 5 = disabled
text: [1, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@label:Printjob", "Prepare") : catalog.i18nc("@label:Printjob", "Cancel") text: [1, 4, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@label:Printjob", "Prepare") : catalog.i18nc("@label:Printjob", "Cancel")
onClicked: onClicked:
{ {
sliceOrStopSlicing(); sliceOrStopSlicing();

View file

@ -1,8 +1,8 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura

View file

@ -1,9 +1,9 @@
// Copyright (c) 2015 Ultimaker B.V. // Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.2 as UM import UM 1.2 as UM

View file

@ -1,8 +1,8 @@
// Copyright (c) 2015 Ultimaker B.V. // Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.1 as UM import UM 1.1 as UM

View file

@ -1,8 +1,8 @@
// Copyright (c) 2016 Ultimaker B.V. // Copyright (c) 2016 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura

View file

@ -1,9 +1,9 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura

View file

@ -1,8 +1,8 @@
// Copyright (c) 2016 Ultimaker B.V. // Copyright (c) 2016 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.1 as UM import UM 1.1 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura

View file

@ -1,8 +1,8 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.1 as UM import UM 1.1 as UM
@ -135,14 +135,6 @@ SettingItem
} }
} }
onEditingFinished:
{
if (textHasChanged)
{
propertyProvider.setPropertyValue("value", text)
}
}
onActiveFocusChanged: onActiveFocusChanged:
{ {
if(activeFocus) if(activeFocus)

View file

@ -1,8 +1,8 @@
// Copyright (c) 2015 Ultimaker B.V. // Copyright (c) 2015 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.0
import UM 1.2 as UM import UM 1.2 as UM

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2017 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.8 import QtQuick 2.7
import QtQuick.Controls 1.1 import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1 import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
@ -63,11 +63,9 @@ Item
menu: ProfileMenu { } menu: ProfileMenu { }
function generateActiveQualityText () { function generateActiveQualityText () {
var result = catalog.i18nc("@", "No Profile Available") // default text var result = Cura.MachineManager.activeQualityName;
if (Cura.MachineManager.isActiveQualitySupported ) {
result = Cura.MachineManager.activeQualityName
if (Cura.MachineManager.isActiveQualitySupported) {
if (Cura.MachineManager.activeQualityLayerHeight > 0) { if (Cura.MachineManager.activeQualityLayerHeight > 0) {
result += " <font color=\"" + UM.Theme.getColor("text_detail") + "\">" result += " <font color=\"" + UM.Theme.getColor("text_detail") + "\">"
result += " - " result += " - "

Some files were not shown because too many files have changed in this diff Show more