Merge branch 'master' into feature_headless_docker

This commit is contained in:
ChrisTerBeke 2018-01-12 11:14:47 +01:00
commit 1a05fd5989
198 changed files with 9149 additions and 5490 deletions

1
.gitignore vendored
View file

@ -47,7 +47,6 @@ plugins/Doodle3D-cura-plugin
plugins/FlatProfileExporter
plugins/GodMode
plugins/OctoPrintPlugin
plugins/PostProcessingPlugin
plugins/ProfileFlattener
plugins/X3GWriter

View file

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

View file

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

View file

@ -266,6 +266,7 @@ class CuraApplication(QtApplication):
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity)
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware")
@ -319,7 +320,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/asked_dialog_on_project_save", False)
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/arrange_objects_on_load", True)
preferences.addPreference("cura/not_arrange_objects_on_load", False)
preferences.addPreference("cura/use_multi_build_plate", False)
preferences.addPreference("cura/currency", "")
@ -902,12 +903,17 @@ class CuraApplication(QtApplication):
def getSceneBoundingBoxString(self):
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
## Update scene bounding box for current build plate
def updatePlatformActivity(self, node = None):
count = 0
scene_bounding_box = None
is_block_slicing_node = False
active_build_plate = self.getBuildPlateModel().activeBuildPlate
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")):
if (
not issubclass(type(node), CuraSceneNode) or
(not node.getMeshData() and not node.callDecoration("getLayerData")) or
(node.callDecoration("getBuildPlateNumber") != active_build_plate)):
continue
if node.callDecoration("isBlockSlicing"):
is_block_slicing_node = True
@ -927,7 +933,7 @@ class CuraApplication(QtApplication):
if not scene_bounding_box:
scene_bounding_box = AxisAlignedBox.Null
if repr(self._scene_bounding_box) != repr(scene_bounding_box) and scene_bounding_box.isValid():
if repr(self._scene_bounding_box) != repr(scene_bounding_box):
self._scene_bounding_box = scene_bounding_box
self.sceneBoundingBoxChanged.emit()
@ -1032,7 +1038,7 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
Selection.add(node)
@ -1104,7 +1110,7 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
nodes.append(node)
@ -1131,7 +1137,7 @@ class CuraApplication(QtApplication):
continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
# Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
@ -1154,7 +1160,7 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
# Skip nodes that are too big
@ -1178,7 +1184,7 @@ class CuraApplication(QtApplication):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable():
continue # i.e. node with layer data
if not node.callDecoration("isSliceable"):
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
if node in nodes: # exclude selected node from fixed_nodes
continue
@ -1433,18 +1439,24 @@ class CuraApplication(QtApplication):
filename = job.getFileName()
self._currently_loading_files.remove(filename)
root = self.getController().getScene().getRoot()
arranger = Arrange.create(scene_root = root)
min_offset = 8
self.fileLoaded.emit(filename)
arrange_objects_on_load = (
not Preferences.getInstance().getValue("cura/use_multi_build_plate") or
Preferences.getInstance().getValue("cura/arrange_objects_on_load"))
not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load"))
target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
root = self.getController().getScene().getRoot()
fixed_nodes = []
for node_ in DepthFirstIterator(root):
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
fixed_nodes.append(node_)
arranger = Arrange.create(fixed_nodes = fixed_nodes)
min_offset = 8
for original_node in nodes:
node = CuraSceneNode() # We want our own CuraSceneNode
# Create a CuraSceneNode just if the original node is not that type
node = original_node if isinstance(original_node, CuraSceneNode) else CuraSceneNode()
node.setMeshData(original_node.getMeshData())
node.setSelectable(True)
@ -1489,7 +1501,14 @@ class CuraApplication(QtApplication):
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
node.addDecorator(BuildPlateDecorator(target_build_plate))
# This node is deepcopied from some other node which already has a BuildPlateDecorator, but the deepcopy
# of BuildPlateDecorator produces one that's assoicated with build plate -1. So, here we need to check if
# the BuildPlateDecorator exists or not and always set the correct build plate number.
build_plate_decorator = node.getDecorator(BuildPlateDecorator)
if build_plate_decorator is None:
build_plate_decorator = BuildPlateDecorator(target_build_plate)
node.addDecorator(build_plate_decorator)
build_plate_decorator.setBuildPlateNumber(target_build_plate)
op = AddSceneNodeOperation(node, scene.getRoot())
op.push()

View file

@ -4,7 +4,9 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Preferences import Preferences
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Keep track of all objects in the project
class ObjectsModel(ListModel):
@ -24,16 +26,28 @@ class ObjectsModel(ListModel):
nodes = []
filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate")
active_build_plate_number = self._build_plate_number
group_nr = 1
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")):
if not issubclass(type(node), SceneNode):
continue
if not node.callDecoration("isSliceable"):
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
continue
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
if filter_current_build_plate and node_build_plate_number != active_build_plate_number:
continue
if not node.callDecoration("isGroup"):
name = node.getName()
else:
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
group_nr += 1
nodes.append({
"name": node.getName(),
"name": name,
"isSelected": Selection.isSelected(node),
"isOutsideBuildArea": node.isOutsideBuildArea(),
"buildPlateNumber": node_build_plate_number,

View file

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

View file

@ -11,6 +11,7 @@ from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager
from typing import Dict
import math
import os.path
@ -177,7 +178,7 @@ class PrintInformation(QObject):
self._material_amounts = material_amounts
self._calculateInformation(build_plate_number)
def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time):
def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]):
total_estimated_time = 0
if build_plate_number not in self._print_time_message_values:

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.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
from PyQt5.QtWidgets import QMessageBox
from enum import IntEnum # For the connection state tracking.
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger
from UM.Signal import signalemitter
from UM.Application import Application
from enum import IntEnum # For the connection state tracking.
from typing import List, Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
i18n_catalog = i18nCatalog("cura")
## Printer output device adds extra interface options on top of output device.
@ -25,663 +31,138 @@ i18n_catalog = i18nCatalog("cura")
# For all other uses it should be used in the same way as a "regular" OutputDevice.
@signalemitter
class PrinterOutputDevice(QObject, OutputDevice):
printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str)
acceptsCommandsChanged = pyqtSignal()
# Signal to indicate that the material of the active printer on the remote changed.
materialIdChanged = pyqtSignal()
# # Signal to indicate that the hotend of the active printer on the remote changed.
hotendIdChanged = pyqtSignal()
def __init__(self, device_id, parent = None):
super().__init__(device_id = device_id, parent = parent)
self._container_registry = ContainerRegistry.getInstance()
self._target_bed_temperature = 0
self._bed_temperature = 0
self._num_extruders = 1
self._hotend_temperatures = [0] * self._num_extruders
self._target_hotend_temperatures = [0] * self._num_extruders
self._material_ids = [""] * self._num_extruders
self._hotend_ids = [""] * self._num_extruders
self._progress = 0
self._head_x = 0
self._head_y = 0
self._head_z = 0
self._connection_state = ConnectionState.closed
self._connection_text = ""
self._time_elapsed = 0
self._time_total = 0
self._job_state = ""
self._job_name = ""
self._error_text = ""
self._accepts_commands = True
self._preheat_bed_timeout = 900 # Default time-out for pre-heating the bed, in seconds.
self._preheat_bed_timer = QTimer() # Timer that tracks how long to preheat still.
self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self.cancelPreheatBed)
self._printer_state = ""
self._printer_type = "unknown"
self._camera_active = False
self._printers = [] # type: List[PrinterOutputModel]
self._monitor_view_qml_path = ""
self._monitor_component = None
self._monitor_item = None
self._control_view_qml_path = ""
self._control_component = None
self._control_item = None
self._qml_context = None
self._can_pause = True
self._can_abort = True
self._can_pre_heat_bed = True
self._can_control_manually = True
self._accepts_commands = False
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
self._update_timer = QTimer()
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
self._update_timer.setSingleShot(False)
self._update_timer.timeout.connect(self._update)
self._connection_state = ConnectionState.closed
self._address = ""
@pyqtProperty(str, constant = True)
def address(self):
return self._address
def materialHotendChangedMessage(self, callback):
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
callback(QMessageBox.Yes)
def isConnected(self):
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
def setConnectionState(self, connection_state):
if self._connection_state != connection_state:
self._connection_state = connection_state
self.connectionStateChanged.emit(self._id)
@pyqtProperty(str, notify = connectionStateChanged)
def connectionState(self):
return self._connection_state
def _update(self):
pass
def _getPrinterByKey(self, key) -> Optional["PrinterOutputModel"]:
for printer in self._printers:
if printer.key == key:
return printer
return None
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
raise NotImplementedError("requestWrite needs to be implemented")
## Signals
@pyqtProperty(QObject, notify = printersChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]:
if len(self._printers):
return self._printers[0]
return None
# Signal to be emitted when bed temp is changed
bedTemperatureChanged = pyqtSignal()
# Signal to be emitted when target bed temp is changed
targetBedTemperatureChanged = pyqtSignal()
# Signal when the progress is changed (usually when this output device is printing / sending lots of data)
progressChanged = pyqtSignal()
# Signal to be emitted when hotend temp is changed
hotendTemperaturesChanged = pyqtSignal()
# Signal to be emitted when target hotend temp is changed
targetHotendTemperaturesChanged = pyqtSignal()
# Signal to be emitted when head position is changed (x,y,z)
headPositionChanged = pyqtSignal()
# Signal to be emitted when either of the material ids is changed
materialIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
# Signal to be emitted when either of the hotend ids is changed
hotendIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
# Signal that is emitted every time connection state is changed.
# it also sends it's own device_id (for convenience sake)
connectionStateChanged = pyqtSignal(str)
connectionTextChanged = pyqtSignal()
timeElapsedChanged = pyqtSignal()
timeTotalChanged = pyqtSignal()
jobStateChanged = pyqtSignal()
jobNameChanged = pyqtSignal()
errorTextChanged = pyqtSignal()
acceptsCommandsChanged = pyqtSignal()
printerStateChanged = pyqtSignal()
printerTypeChanged = pyqtSignal()
# Signal to be emitted when some drastic change occurs in the remaining time (not when the time just passes on normally).
preheatBedRemainingTimeChanged = pyqtSignal()
# Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True)
def canPreHeatBed(self):
return self._can_pre_heat_bed
# Does the printer support pause at all
@pyqtProperty(bool, constant=True)
def canPause(self):
return self._can_pause
# Does the printer support abort at all
@pyqtProperty(bool, constant=True)
def canAbort(self):
return self._can_abort
# Does the printer support manual control at all
@pyqtProperty(bool, constant=True)
def canControlManually(self):
return self._can_control_manually
@pyqtProperty("QVariantList", notify = printersChanged)
def printers(self):
return self._printers
@pyqtProperty(QObject, constant=True)
def monitorItem(self):
# Note that we specifically only check if the monitor component is created.
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
# create the item (and fail) every time.
if not self._monitor_item:
if not self._monitor_component:
self._createMonitorViewFromQML()
return self._monitor_item
@pyqtProperty(QObject, constant=True)
def controlItem(self):
if not self._control_item:
if not self._control_component:
self._createControlViewFromQML()
return self._control_item
def _createControlViewFromQML(self):
if not self._control_view_qml_path:
return
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {
"OutputDevice": self
})
if self._control_item is None:
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
def _createMonitorViewFromQML(self):
if not self._monitor_view_qml_path:
return
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {
"OutputDevice": self
})
if self._monitor_item is None:
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
@pyqtProperty(str, notify=printerTypeChanged)
def printerType(self):
return self._printer_type
## Attempt to establish connection
def connect(self):
self.setConnectionState(ConnectionState.connecting)
self._update_timer.start()
@pyqtProperty(str, notify=printerStateChanged)
def printerState(self):
return self._printer_state
## Attempt to close the connection
def close(self):
self._update_timer.stop()
self.setConnectionState(ConnectionState.closed)
@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()
## Ensure that close gets called when object is destroyed
def __del__(self):
self.close()
@pyqtProperty(bool, notify=acceptsCommandsChanged)
def acceptsCommands(self):
return self._accepts_commands
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
def setAcceptsCommands(self, accepts_commands):
def _setAcceptsCommands(self, accepts_commands):
if self._accepts_commands != accepts_commands:
self._accepts_commands = accepts_commands
self.acceptsCommandsChanged.emit()
## Get the bed temperature of the bed (if any)
# This function is "final" (do not re-implement)
# /sa _getBedTemperature implementation function
@pyqtProperty(float, notify = bedTemperatureChanged)
def bedTemperature(self):
return self._bed_temperature
## Set the (target) bed temperature
# This function is "final" (do not re-implement)
# /param temperature new target temperature of the bed (in deg C)
# /sa _setTargetBedTemperature implementation function
@pyqtSlot(int)
def setTargetBedTemperature(self, temperature):
self._setTargetBedTemperature(temperature)
if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
## The total duration of the time-out to pre-heat the bed, in seconds.
#
# \return The duration of the time-out to pre-heat the bed, in seconds.
@pyqtProperty(int, constant = True)
def preheatBedTimeout(self):
return self._preheat_bed_timeout
## The remaining duration of the pre-heating of the bed.
#
# This is formatted in M:SS format.
# \return The duration of the time-out to pre-heat the bed, formatted.
@pyqtProperty(str, notify = preheatBedRemainingTimeChanged)
def preheatBedRemainingTime(self):
if not self._preheat_bed_timer.isActive():
return ""
period = self._preheat_bed_timer.remainingTime()
if period <= 0:
return ""
minutes, period = divmod(period, 60000) #60000 milliseconds in a minute.
seconds, _ = divmod(period, 1000) #1000 milliseconds in a second.
if minutes <= 0 and seconds <= 0:
return ""
return "%d:%02d" % (minutes, seconds)
## Time the print has been printing.
# Note that timeTotal - timeElapsed should give time remaining.
@pyqtProperty(float, notify = timeElapsedChanged)
def timeElapsed(self):
return self._time_elapsed
## Total time of the print
# Note that timeTotal - timeElapsed should give time remaining.
@pyqtProperty(float, notify=timeTotalChanged)
def timeTotal(self):
return self._time_total
@pyqtSlot(float)
def setTimeTotal(self, new_total):
if self._time_total != new_total:
self._time_total = new_total
self.timeTotalChanged.emit()
@pyqtSlot(float)
def setTimeElapsed(self, time_elapsed):
if self._time_elapsed != time_elapsed:
self._time_elapsed = time_elapsed
self.timeElapsedChanged.emit()
## Home the head of the connected printer
# This function is "final" (do not re-implement)
# /sa _homeHead implementation function
@pyqtSlot()
def homeHead(self):
self._homeHead()
## Home the head of the connected printer
# This is an implementation function and should be overriden by children.
def _homeHead(self):
Logger.log("w", "_homeHead is not implemented by this output device")
## Home the bed of the connected printer
# This function is "final" (do not re-implement)
# /sa _homeBed implementation function
@pyqtSlot()
def homeBed(self):
self._homeBed()
## Home the bed of the connected printer
# This is an implementation function and should be overriden by children.
# /sa homeBed
def _homeBed(self):
Logger.log("w", "_homeBed is not implemented by this output device")
## Protected setter for the bed temperature of the connected printer (if any).
# /parameter temperature Temperature bed needs to go to (in deg celsius)
# /sa setTargetBedTemperature
def _setTargetBedTemperature(self, temperature):
Logger.log("w", "_setTargetBedTemperature is not implemented by this output device")
## Pre-heats the heated bed of the printer.
#
# \param temperature The temperature to heat the bed to, in degrees
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float)
def preheatBed(self, temperature, duration):
Logger.log("w", "preheatBed is not implemented by this output device.")
## Cancels pre-heating the heated bed of the printer.
#
# If the bed is not pre-heated, nothing happens.
@pyqtSlot()
def cancelPreheatBed(self):
Logger.log("w", "cancelPreheatBed is not implemented by this output device.")
## Protected setter for the current bed temperature.
# This simply sets the bed temperature, but ensures that a signal is emitted.
# /param temperature temperature of the bed.
def _setBedTemperature(self, temperature):
if self._bed_temperature != temperature:
self._bed_temperature = temperature
self.bedTemperatureChanged.emit()
## Get the target bed temperature if connected printer (if any)
@pyqtProperty(int, notify = targetBedTemperatureChanged)
def targetBedTemperature(self):
return self._target_bed_temperature
## Set the (target) hotend temperature
# This function is "final" (do not re-implement)
# /param index the index of the hotend that needs to change temperature
# /param temperature The temperature it needs to change to (in deg celsius).
# /sa _setTargetHotendTemperature implementation function
@pyqtSlot(int, int)
def setTargetHotendTemperature(self, index, temperature):
self._setTargetHotendTemperature(index, temperature)
if self._target_hotend_temperatures[index] != temperature:
self._target_hotend_temperatures[index] = temperature
self.targetHotendTemperaturesChanged.emit()
## Implementation function of setTargetHotendTemperature.
# /param index Index of the hotend to set the temperature of
# /param temperature Temperature to set the hotend to (in deg C)
# /sa setTargetHotendTemperature
def _setTargetHotendTemperature(self, index, temperature):
Logger.log("w", "_setTargetHotendTemperature is not implemented by this output device")
@pyqtProperty("QVariantList", notify = targetHotendTemperaturesChanged)
def targetHotendTemperatures(self):
return self._target_hotend_temperatures
@pyqtProperty("QVariantList", notify = hotendTemperaturesChanged)
def hotendTemperatures(self):
return self._hotend_temperatures
## Protected setter for the current hotend temperature.
# This simply sets the hotend temperature, but ensures that a signal is emitted.
# /param index Index of the hotend
# /param temperature temperature of the hotend (in deg C)
def _setHotendTemperature(self, index, temperature):
if self._hotend_temperatures[index] != temperature:
self._hotend_temperatures[index] = temperature
self.hotendTemperaturesChanged.emit()
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialIds(self):
return self._material_ids
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialNames(self):
result = []
for material_id in self._material_ids:
if material_id is None:
result.append(i18n_catalog.i18nc("@item:material", "No material loaded"))
continue
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0]["name"])
else:
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
return result
## List of the colours of the currently loaded materials.
#
# The list is in order of extruders. If there is no material in an
# extruder, the colour is shown as transparent.
#
# The colours are returned in hex-format AARRGGBB or RRGGBB
# (e.g. #800000ff for transparent blue or #00ff00 for pure green).
@pyqtProperty("QVariantList", notify = materialIdChanged)
def materialColors(self):
result = []
for material_id in self._material_ids:
if material_id is None:
result.append("#00000000") #No material.
continue
containers = self._container_registry.findInstanceContainersMetadata(type = "material", GUID = material_id)
if containers:
result.append(containers[0]["color_code"])
else:
result.append("#00000000") #Unknown material.
return result
## Protected setter for the current material id.
# /param index Index of the extruder
# /param material_id id of the material
def _setMaterialId(self, index, material_id):
if material_id and material_id != "" and material_id != self._material_ids[index]:
Logger.log("d", "Setting material id of hotend %d to %s" % (index, material_id))
self._material_ids[index] = material_id
self.materialIdChanged.emit(index, material_id)
@pyqtProperty("QVariantList", notify = hotendIdChanged)
def hotendIds(self):
return self._hotend_ids
## Protected setter for the current hotend id.
# /param index Index of the extruder
# /param hotend_id id of the hotend
def _setHotendId(self, index, hotend_id):
if hotend_id and hotend_id != self._hotend_ids[index]:
Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id))
self._hotend_ids[index] = hotend_id
self.hotendIdChanged.emit(index, hotend_id)
elif not hotend_id:
Logger.log("d", "Removing hotend id of hotend %d.", index)
self._hotend_ids[index] = None
self.hotendIdChanged.emit(index, None)
## Let the user decide if the hotends and/or material should be synced with the printer
# NB: the UX needs to be implemented by the plugin
def materialHotendChangedMessage(self, callback):
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
callback(QMessageBox.Yes)
## Attempt to establish connection
def connect(self):
raise NotImplementedError("connect needs to be implemented")
## Attempt to close the connection
def close(self):
raise NotImplementedError("close needs to be implemented")
@pyqtProperty(bool, notify = connectionStateChanged)
def connectionState(self):
return self._connection_state
## Set the connection state of this output device.
# /param connection_state ConnectionState enum.
def setConnectionState(self, connection_state):
if self._connection_state != connection_state:
self._connection_state = connection_state
self.connectionStateChanged.emit(self._id)
@pyqtProperty(str, notify = connectionTextChanged)
def connectionText(self):
return self._connection_text
## Set a text that is shown on top of the print monitor tab
def setConnectionText(self, connection_text):
if self._connection_text != connection_text:
self._connection_text = connection_text
self.connectionTextChanged.emit()
## Ensure that close gets called when object is destroyed
def __del__(self):
self.close()
## Get the x position of the head.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headX(self):
return self._head_x
## Get the y position of the head.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headY(self):
return self._head_y
## Get the z position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
@pyqtProperty(float, notify = headPositionChanged)
def headZ(self):
return self._head_z
## Update the saved position of the head
# This function should be called when a new position for the head is received.
def _updateHeadPosition(self, x, y ,z):
position_changed = False
if self._head_x != x:
self._head_x = x
position_changed = True
if self._head_y != y:
self._head_y = y
position_changed = True
if self._head_z != z:
self._head_z = z
position_changed = True
if position_changed:
self.headPositionChanged.emit()
## Set the position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadPosition implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def setHeadPosition(self, x, y, z, speed = 3000):
self._setHeadPosition(x, y , z, speed)
## Set the X position of the head.
# This function is "final" (do not re-implement)
# /param x x position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadx implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadX(self, x, speed = 3000):
self._setHeadX(x, speed)
## Set the Y position of the head.
# This function is "final" (do not re-implement)
# /param y y position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadY(self, y, speed = 3000):
self._setHeadY(y, speed)
## Set the Z position of the head.
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
# This function is "final" (do not re-implement)
# /param z z position head needs to move to.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ implementation function
@pyqtSlot("long")
@pyqtSlot("long", "long")
def setHeadZ(self, z, speed = 3000):
self._setHeadZ(z, speed)
## Move the head of the printer.
# Note that this is a relative move. If you want to move the head to a specific position you can use
# setHeadPosition
# This function is "final" (do not re-implement)
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _moveHead implementation function
@pyqtSlot("long", "long", "long")
@pyqtSlot("long", "long", "long", "long")
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
self._moveHead(x, y, z, speed)
## Implementation function of moveHead.
# /param x distance in x to move
# /param y distance in y to move
# /param z distance in z to move
# /param speed Speed by which it needs to move (in mm/minute)
# /sa moveHead
def _moveHead(self, x, y, z, speed):
Logger.log("w", "_moveHead is not implemented by this output device")
## Implementation function of setHeadPosition.
# /param x new x location of the head.
# /param y new y location of the head.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadPosition
def _setHeadPosition(self, x, y, z, speed):
Logger.log("w", "_setHeadPosition is not implemented by this output device")
## Implementation function of setHeadX.
# /param x new x location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa setHeadX
def _setHeadX(self, x, speed):
Logger.log("w", "_setHeadX is not implemented by this output device")
## Implementation function of setHeadY.
# /param y new y location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadY
def _setHeadY(self, y, speed):
Logger.log("w", "_setHeadY is not implemented by this output device")
## Implementation function of setHeadZ.
# /param z new z location of the head.
# /param speed Speed by which it needs to move (in mm/minute)
# /sa _setHeadZ
def _setHeadZ(self, z, speed):
Logger.log("w", "_setHeadZ is not implemented by this output device")
## Get the progress of any currently active process.
# This function is "final" (do not re-implement)
# /sa _getProgress
# /returns float progress of the process. -1 indicates that there is no process.
@pyqtProperty(float, notify = progressChanged)
def progress(self):
return self._progress
## Set the progress of any currently active process
# /param progress Progress of the process.
def setProgress(self, progress):
if self._progress != progress:
self._progress = progress
self.progressChanged.emit()
## The current processing state of the backend.
class ConnectionState(IntEnum):

View file

@ -10,9 +10,12 @@ from UM.Application import Application
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Signal import Signal
class CuraSceneController(QObject):
activeBuildPlateChanged = Signal()
def __init__(self, objects_model: ObjectsModel, build_plate_model: BuildPlateModel):
super().__init__()
@ -41,6 +44,14 @@ class CuraSceneController(QObject):
self._build_plate_model.setMaxBuildPlate(self._max_build_plate)
build_plates = [{"name": "Build Plate %d" % (i + 1), "buildPlateNumber": i} for i in range(self._max_build_plate + 1)]
self._build_plate_model.setItems(build_plates)
if self._active_build_plate > self._max_build_plate:
build_plate_number = 0
if self._last_selected_index >= 0: # go to the buildplate of the item you last selected
item = self._objects_model.getItem(self._last_selected_index)
if "node" in item:
node = item["node"]
build_plate_number = node.callDecoration("getBuildPlateNumber")
self.setActiveBuildPlate(build_plate_number)
# self.buildPlateItemsChanged.emit() # TODO: necessary after setItems?
def _calcMaxBuildPlate(self):
@ -75,11 +86,11 @@ class CuraSceneController(QObject):
# Single select
item = self._objects_model.getItem(index)
node = item["node"]
Selection.clear()
Selection.add(node)
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != -1:
self._build_plate_model.setActiveBuildPlate(build_plate_number)
self.setActiveBuildPlate(build_plate_number)
Selection.clear()
Selection.add(node)
self._last_selected_index = index
@ -93,6 +104,7 @@ class CuraSceneController(QObject):
self._build_plate_model.setActiveBuildPlate(nr)
self._objects_model.setActiveBuildPlate(nr)
self.activeBuildPlateChanged.emit()
@staticmethod
def createCuraSceneController():

View file

@ -816,6 +816,22 @@ class ContainerManager(QObject):
ContainerRegistry.getInstance().addContainer(container_to_add)
return self._getMaterialContainerIdForActiveMachine(clone_of_original)
## Create a duplicate of a material or it's original entry
#
# \return \type{str} the id of the newly created container.
@pyqtSlot(str, result = str)
def duplicateOriginalMaterial(self, material_id):
# check if the given material has a base file (i.e. was shipped by default)
base_file = self.getContainerMetaDataEntry(material_id, "base_file")
if base_file == "":
# there is no base file, so duplicate by ID
return self.duplicateMaterial(material_id)
else:
# there is a base file, so duplicate the original material
return self.duplicateMaterial(base_file)
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
#
# \return \type{str} the id of the newly created container.

View file

@ -202,7 +202,6 @@ class CuraContainerRegistry(ContainerRegistry):
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
if meta_data["profile_reader"][0]["extension"] != extension:
continue
profile_reader = plugin_registry.getPluginObject(plugin_id)
try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
@ -269,6 +268,10 @@ class CuraContainerRegistry(ContainerRegistry):
profile._id = new_id
profile.setName(new_name)
# Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile
# It also solves an issue with importing profiles from G-Codes
profile.setMetaDataEntry("id", new_id)
if "type" in profile.getMetaData():
profile.setMetaDataEntry("type", "quality_changes")
else:
@ -515,6 +518,7 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id)
if extruder_quality_changes_container:
extruder_quality_changes_container = extruder_quality_changes_container[0]
quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id)
else:
@ -525,15 +529,92 @@ class CuraContainerRegistry(ContainerRegistry):
if extruder_quality_changes_container:
quality_changes_id = extruder_quality_changes_container.getId()
extruder_stack.setQualityChangesById(quality_changes_id)
else:
# if we still cannot find a quality changes container for the extruder, create a new one
container_id = self.uniqueName(extruder_stack.getId() + "_user")
container_name = machine.qualityChanges.getName()
extruder_quality_changes_container = InstanceContainer(container_id)
extruder_quality_changes_container.setName(container_name)
extruder_quality_changes_container.addMetaDataEntry("type", "quality_changes")
extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId())
extruder_quality_changes_container.addMetaDataEntry("quality_type", machine.qualityChanges.getMetaDataEntry("quality_type"))
extruder_quality_changes_container.setDefinition(machine.qualityChanges.getDefinition().getId())
if not extruder_quality_changes_container:
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
machine.qualityChanges.getName(), extruder_stack.getId())
else:
# move all per-extruder settings to the extruder's quality changes
for qc_setting_key in machine.qualityChanges.getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder:
setting_value = machine.qualityChanges.getProperty(qc_setting_key, "value")
setting_definition = machine.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
extruder_quality_changes_container.addInstance(new_instance)
extruder_quality_changes_container.setDirty(True)
machine.qualityChanges.removeInstance(qc_setting_key, postpone_emit=True)
else:
extruder_stack.setQualityChangesById("empty_quality_changes")
self.addContainer(extruder_stack)
# Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have
# per-extruder settings in the container for the machine instead of the extruder.
if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"):
quality_changes_machine_definition_id = machine.qualityChanges.getDefinition().getId()
else:
whole_machine_definition = machine.definition
machine_entry = machine.definition.getMetaDataEntry("machine")
if machine_entry is not None:
container_registry = ContainerRegistry.getInstance()
whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0]
quality_changes_machine_definition_id = "fdmprinter"
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition",
whole_machine_definition.getId())
qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id)
qc_groups = {} # map of qc names -> qc containers
for qc in qcs:
qc_name = qc.getName()
if qc_name not in qc_groups:
qc_groups[qc_name] = []
qc_groups[qc_name].append(qc)
# try to find from the quality changes cura directory too
quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName())
if quality_changes_container:
qc_groups[qc_name].append(quality_changes_container)
for qc_name, qc_list in qc_groups.items():
qc_dict = {"global": None, "extruders": []}
for qc in qc_list:
extruder_def_id = qc.getMetaDataEntry("extruder")
if extruder_def_id is not None:
qc_dict["extruders"].append(qc)
else:
qc_dict["global"] = qc
if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
# move per-extruder settings
for qc_setting_key in qc_dict["global"].getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder:
setting_value = qc_dict["global"].getProperty(qc_setting_key, "value")
setting_definition = machine.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, definition_changes)
new_instance.setProperty("value", setting_value)
new_instance.resetState() # Ensure that the state is not seen as a user state.
qc_dict["extruders"][0].addInstance(new_instance)
qc_dict["extruders"][0].setDirty(True)
qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True)
# Set next stack at the end
extruder_stack.setNextStack(machine)
@ -562,6 +643,9 @@ class CuraContainerRegistry(ContainerRegistry):
if parser["general"]["name"] == name:
# load the container
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
if self.findInstanceContainers(id = container_id):
# this container is already in the registry, skip it
continue
instance_container = InstanceContainer(container_id)
with open(file_path, "r") as f:

View file

@ -144,8 +144,7 @@ class MachineManager(QObject):
printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged)
printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)
self._printer_output_devices.clear()
self._printer_output_devices = []
for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
if isinstance(printer_output_device, PrinterOutputDevice):
self._printer_output_devices.append(printer_output_device)
@ -170,58 +169,70 @@ class MachineManager(QObject):
def totalNumberOfSettings(self) -> int:
return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys())
def _onHotendIdChanged(self, index: Union[str, int], hotend_id: str) -> None:
if not self._global_container_stack:
def _onHotendIdChanged(self):
if not self._global_container_stack or not self._printer_output_devices:
return
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "variant", definition = self._global_container_stack.definition.getId(), name = hotend_id)
if containers: # New material ID is known
extruder_manager = ExtruderManager.getInstance()
active_printer_model = self._printer_output_devices[0].activePrinter
if not active_printer_model:
return
change_found = False
machine_id = self.activeMachineId
extruders = extruder_manager.getMachineExtruders(machine_id)
matching_extruder = None
for extruder in extruders:
if str(index) == extruder.getMetaDataEntry("position"):
matching_extruder = extruder
break
if matching_extruder and matching_extruder.variant.getName() != hotend_id:
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
self._auto_hotends_changed[str(index)] = containers[0]["id"]
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
else:
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.definition.getId(), hotend_id))
extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
key=lambda k: k.getMetaDataEntry("position"))
def _onMaterialIdChanged(self, index: Union[str, int], material_id: str):
if not self._global_container_stack:
for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="variant",
definition=self._global_container_stack.definition.getId(),
name=extruder_model.hotendID)
if containers:
# The hotend ID is known.
machine_id = self.activeMachineId
if extruder.variant.getName() != extruder_model.hotendID:
change_found = True
self._auto_hotends_changed[extruder.getMetaDataEntry("position")] = containers[0]["id"]
if change_found:
# A change was found, let the output device handle this.
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
def _onMaterialIdChanged(self):
if not self._global_container_stack or not self._printer_output_devices:
return
definition_id = "fdmprinter"
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
definition_id = self.activeQualityDefinitionId
extruder_manager = ExtruderManager.getInstance()
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", definition = definition_id, GUID = material_id)
if containers: # New material ID is known
extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId))
matching_extruder = None
for extruder in extruders:
if str(index) == extruder.getMetaDataEntry("position"):
matching_extruder = extruder
break
active_printer_model = self._printer_output_devices[0].activePrinter
if not active_printer_model:
return
if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id:
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
if self._global_container_stack.definition.getMetaDataEntry("has_variants") and matching_extruder.variant:
variant_id = self.getQualityVariantId(self._global_container_stack.definition, matching_extruder.variant)
change_found = False
machine_id = self.activeMachineId
extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id),
key=lambda k: k.getMetaDataEntry("position"))
for extruder_model, extruder in zip(active_printer_model.extruders, extruders):
if extruder_model.activeMaterial is None:
continue
containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="material",
definition=self._global_container_stack.definition.getId(),
GUID=extruder_model.activeMaterial.guid)
if containers:
# The material is known.
if extruder.material.getMetaDataEntry("GUID") != extruder_model.activeMaterial.guid:
change_found = True
if self._global_container_stack.definition.getMetaDataEntry("has_variants") and extruder.variant:
variant_id = self.getQualityVariantId(self._global_container_stack.definition,
extruder.variant)
for container in containers:
if container.get("variant") == variant_id:
self._auto_materials_changed[str(index)] = container["id"]
self._auto_materials_changed[extruder.getMetaDataEntry("position")] = container["id"]
break
else:
# Just use the first result we found.
self._auto_materials_changed[str(index)] = containers[0]["id"]
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)
else:
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
def _materialHotendChangedCallback(self, button):
if button == QMessageBox.No:

View file

@ -40,7 +40,7 @@ import faulthandler
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
linux_distro_name = platform.linux_distribution()[0].lower()
if linux_distro_name in ("debian", "ubuntu", "linuxmint", "fedora"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
# TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
import ctypes
from ctypes.util import find_library
libGL = find_library("GL")

View file

@ -88,7 +88,6 @@ class CuraEngineBackend(QObject, Backend):
#
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@ -198,7 +197,6 @@ class CuraEngineBackend(QObject, Backend):
self._slice_start_time = time()
if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0)
self.backendStateChange.emit(BackendState.Done)
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
return
@ -206,8 +204,8 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", " ## Process layers job still busy, trying later")
return
if not hasattr(self._scene, "gcode_list"):
self._scene.gcode_list = {}
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {}
# see if we really have to slice
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
@ -215,8 +213,10 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
num_objects = self._numObjects()
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
self._scene.gcode_list[build_plate_to_be_sliced] = []
Logger.log("d", "Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced)
self._scene.gcode_dict[build_plate_to_be_sliced] = []
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
if self._build_plates_to_be_sliced:
self.slice()
return
self._stored_layer_data = []
@ -233,10 +233,12 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
self._scene.gcode_list[build_plate_to_be_sliced] = [] #[] indexed by build plate number
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number
self._slicing = True
self.slicingStarted.emit()
self.determineAutoSlicing() # Switch timer on or off if appropriate
slice_message = self._socket.createMessage("cura.proto.Slice")
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
self._start_slice_job_build_plate = build_plate_to_be_sliced
@ -357,6 +359,18 @@ class CuraEngineBackend(QObject, Backend):
else:
self.backendStateChange.emit(BackendState.NotStarted)
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
if Application.getInstance().platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
else:
self.backendStateChange.emit(BackendState.NotStarted)
pass
self._invokeSlice()
return
# Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage())
@ -380,7 +394,7 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Disabled)
gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None:
self._scene.gcode_list[node.callDecoration("getBuildPlateNumber")] = gcode_list
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list
if self._use_timer == enable_timer:
return self._use_timer
@ -445,6 +459,7 @@ class CuraEngineBackend(QObject, Backend):
for build_plate_number in build_plate_changed:
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
self.printDurationMessage.emit(source_build_plate_number, {}, [])
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
# if not self._use_timer:
@ -507,7 +522,7 @@ class CuraEngineBackend(QObject, Backend):
def _onStackErrorCheckFinished(self):
self._is_error_check_scheduled = False
if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing:
if not self._slicing and self._build_plates_to_be_sliced:
self.needsSlicing()
self._onChanged()
@ -547,7 +562,7 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Done)
self.processingProgress.emit(1.0)
gcode_list = self._scene.gcode_list[self._start_slice_job_build_plate]
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate]
for index, line in enumerate(gcode_list):
replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
@ -570,21 +585,23 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "See if there is more to slice...")
# Somehow this results in an Arcus Error
# self.slice()
# Testing call slice again, allow backend to restart by using the timer
# Call slice again using the timer, allowing the backend to restart
if self._build_plates_to_be_sliced:
self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode
self._invokeSlice()
## Called when a g-code message is received from the engine.
#
# \param message The protobuf message containing g-code, encoded as UTF-8.
def _onGCodeLayerMessage(self, message):
self._scene.gcode_list[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
## Called when a g-code prefix message is received from the engine.
#
# \param message The protobuf message containing the g-code prefix,
# encoded as UTF-8.
def _onGCodePrefixMessage(self, message):
self._scene.gcode_list[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
## Creates a new socket connection.
def _createSocket(self):
@ -704,7 +721,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack.containersChanged.disconnect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingChanged)
@ -715,7 +732,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
self._global_container_stack.containersChanged.connect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingChanged)
extruder.containersChanged.connect(self._onChanged)

View file

@ -12,4 +12,6 @@ class ProcessGCodeLayerJob(Job):
self._message = message
def run(self):
self._scene.gcode_list.append(self._message.data.decode("utf-8", "replace"))
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = self._scene.gcode_dict[active_build_plate_id]
gcode_list.append(self._message.data.decode("utf-8", "replace"))

View file

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

View file

@ -8,14 +8,14 @@ from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerDataDecorator import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.Scene.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
@ -292,7 +292,7 @@ class FlavorParser:
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
scene_node = SceneNode()
scene_node = CuraSceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
@ -418,11 +418,17 @@ class FlavorParser:
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator = LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
@ -430,7 +436,10 @@ class FlavorParser:
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
# gcode_dict stores gcode_lists for a number of build plates.
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_dict = {active_build_plate_id: gcode_list}
Application.getInstance().getController().getScene().gcode_dict = gcode_dict
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()

View file

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

View file

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

View file

@ -14,60 +14,122 @@ class MonitorStage(CuraStage):
super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail
Application.getInstance().engineCreatedSignal.connect(self._setComponents)
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
self._printer_output_device = None
# Update the status icon when the output device is changed
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
self._active_print_job = None
self._active_printer = None
def _setComponents(self):
self._setMainOverlay()
self._setSidebar()
self._setIconSource()
def _setActivePrintJob(self, print_job):
if self._active_print_job != print_job:
if self._active_print_job:
self._active_print_job.stateChanged.disconnect(self._updateIconSource)
self._active_print_job = print_job
if self._active_print_job:
self._active_print_job.stateChanged.connect(self._updateIconSource)
def _setMainOverlay(self):
# Ensure that the right icon source is returned.
self._updateIconSource()
def _setActivePrinter(self, printer):
if self._active_printer != printer:
if self._active_printer:
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
self._active_printer = printer
if self._active_printer:
self._setActivePrintJob(self._active_printer.activePrintJob)
# Jobs might change, so we need to listen to it's changes.
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
else:
self._setActivePrintJob(None)
# Ensure that the right icon source is returned.
self._updateIconSource()
def _onActivePrintJobChanged(self):
self._setActivePrintJob(self._active_printer.activePrintJob)
def _onActivePrinterChanged(self):
self._setActivePrinter(self._printer_output_device.activePrinter)
def _onOutputDevicesChanged(self):
try:
# We assume that you are monitoring the device with the highest priority.
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
if new_output_device != self._printer_output_device:
if self._printer_output_device:
self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
self._printer_output_device = new_output_device
self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource)
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
self._setActivePrinter(self._printer_output_device.activePrinter)
# Force an update of the icon source
self._updateIconSource()
except IndexError:
pass
def _onEngineCreated(self):
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
self._onOutputDevicesChanged()
self._updateMainOverlay()
self._updateSidebar()
self._updateIconSource()
def _updateMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path)
def _setSidebar(self):
def _updateSidebar(self):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)
def _setIconSource(self):
def _updateIconSource(self):
if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self):
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
if not output_device:
# We assume that you are monitoring the device with the highest priority.
try:
output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
except IndexError:
return "tab_status_unknown"
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
if not output_device.acceptsCommands:
return "tab_status_unknown"
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
return "tab_status_unknown"
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.printerState == "maintenance":
return "tab_status_busy"
if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.jobState == "wait_cleanup":
return "tab_status_finished"
if output_device.jobState in ["ready", ""]:
if output_device.activePrinter is None:
return "tab_status_connected"
if output_device.jobState == "paused":
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.activePrinter.state == "maintenance":
return "tab_status_busy"
if output_device.activePrinter.activePrintJob is None:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.activePrinter.activePrintJob.state == "wait_cleanup":
return "tab_status_finished"
if output_device.activePrinter.activePrintJob.state in ["ready", ""]:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state == "paused":
return "tab_status_paused"
if output_device.jobState == "error":
if output_device.activePrinter.activePrintJob.state == "error":
return "tab_status_stopped"
return "tab_status_unknown"

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -22,6 +23,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
self._node = None
self._stack = None
# this is a set of settings that will be skipped if the user chooses to reset.
self._skip_reset_setting_set = set()
def setSelectedObjectId(self, id):
if id != self._selected_object_id:
self._selected_object_id = id
@ -36,6 +40,10 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
def selectedObjectId(self):
return self._selected_object_id
@pyqtSlot(str)
def addSkipResetSetting(self, setting_name):
self._skip_reset_setting_set.add(setting_name)
def setVisible(self, visible):
if not self._node:
return
@ -50,6 +58,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
# Remove all instances that are not in visibility list
for instance in all_instances:
# exceptionally skip setting
if instance.definition.key in self._skip_reset_setting_set:
continue
if instance.definition.key not in visible:
settings.removeInstance(instance.definition.key)
visibility_changed = True

View file

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

View file

@ -0,0 +1,206 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Application import Application
from UM.Extension import Extension
from UM.Logger import Logger
import os.path
import pkgutil
import sys
import importlib.util
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
# g-code files.
class PostProcessingPlugin(QObject, Extension):
def __init__(self, parent = None):
super().__init__(parent)
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
self._view = None
# Loaded scripts are all scripts that can be used
self._loaded_scripts = {}
self._script_labels = {}
# Script list contains instances of scripts in loaded_scripts.
# There can be duplicates, which will be executed in sequence.
self._script_list = []
self._selected_script_index = -1
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
selectedIndexChanged = pyqtSignal()
@pyqtProperty("QVariant", notify = selectedIndexChanged)
def selectedScriptDefinitionId(self):
try:
return self._script_list[self._selected_script_index].getDefinitionId()
except:
return ""
@pyqtProperty("QVariant", notify=selectedIndexChanged)
def selectedScriptStackId(self):
try:
return self._script_list[self._selected_script_index].getStackId()
except:
return ""
## Execute all post-processing scripts on the gcode.
def execute(self, output_device):
scene = Application.getInstance().getController().getScene()
gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict:
return
# get gcode list for the active build plate
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
return
if ";POSTPROCESSED" not in gcode_list[0]:
for script in self._script_list:
try:
gcode_list = script.execute(gcode_list)
except Exception:
Logger.logException("e", "Exception in post-processing script.")
if len(self._script_list): # Add comment to g-code if any changes were made.
gcode_list[0] += ";POSTPROCESSED\n"
gcode_dict[active_build_plate_id] = gcode_list
setattr(scene, "gcode_dict", gcode_dict)
else:
Logger.log("e", "Already post processed")
@pyqtSlot(int)
def setSelectedScriptIndex(self, index):
self._selected_script_index = index
self.selectedIndexChanged.emit()
@pyqtProperty(int, notify = selectedIndexChanged)
def selectedScriptIndex(self):
return self._selected_script_index
@pyqtSlot(int, int)
def moveScript(self, index, new_index):
if new_index < 0 or new_index > len(self._script_list) - 1:
return # nothing needs to be done
else:
# Magical switch code.
self._script_list[new_index], self._script_list[index] = self._script_list[index], self._script_list[new_index]
self.scriptListChanged.emit()
self.selectedIndexChanged.emit() #Ensure that settings are updated
self._propertyChanged()
## Remove a script from the active script list by index.
@pyqtSlot(int)
def removeScriptByIndex(self, index):
self._script_list.pop(index)
if len(self._script_list) - 1 < self._selected_script_index:
self._selected_script_index = len(self._script_list) - 1
self.scriptListChanged.emit()
self.selectedIndexChanged.emit() # Ensure that settings are updated
self._propertyChanged()
## Load all scripts from provided path.
# This should probably only be done on init.
# \param path Path to check for scripts.
def loadAllScripts(self, path):
scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts:
# Iterate over all scripts.
if script_name not in sys.modules:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script
loaded_class = getattr(loaded_script, script_name)
temp_object = loaded_class()
Logger.log("d", "Begin loading of script: %s", script_name)
try:
setting_data = temp_object.getSettingData()
if "name" in setting_data and "key" in setting_data:
self._script_labels[setting_data["key"]] = setting_data["name"]
self._loaded_scripts[setting_data["key"]] = loaded_class
else:
Logger.log("w", "Script %s.py has no name or key", script_name)
self._script_labels[script_name] = script_name
self._loaded_scripts[script_name] = loaded_class
except AttributeError:
Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
except NotImplementedError:
Logger.log("e", "Script %s.py has no implemented settings", script_name)
self.loadedScriptListChanged.emit()
loadedScriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = loadedScriptListChanged)
def loadedScriptList(self):
return sorted(list(self._loaded_scripts.keys()))
@pyqtSlot(str, result = str)
def getScriptLabelByKey(self, key):
return self._script_labels[key]
scriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = scriptListChanged)
def scriptList(self):
script_list = [script.getSettingData()["key"] for script in self._script_list]
return script_list
@pyqtSlot(str)
def addScriptToList(self, key):
Logger.log("d", "Adding script %s to list.", key)
new_script = self._loaded_scripts[key]()
self._script_list.append(new_script)
self.setSelectedScriptIndex(len(self._script_list) - 1)
self.scriptListChanged.emit()
self._propertyChanged()
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self):
Logger.log("d", "Creating post processing plugin view.")
## Load all scripts in the scripts folders
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]:
try:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
self.loadAllScripts(path)
except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
# Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
Logger.log("d", "Post processing view created.")
# Create the save button component
Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
## Show the (GUI) popup of the post processing plugin.
def showPopup(self):
if self._view is None:
self._createView()
self._view.show()
## Property changed: trigger re-slice
# To do this we use the global container stack propertyChanged.
# Re-slicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode
def _propertyChanged(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")

View file

@ -0,0 +1,501 @@
// Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import UM 1.2 as UM
import Cura 1.0 as Cura
UM.Dialog
{
id: dialog
title: catalog.i18nc("@title:window", "Post Processing Plugin")
width: 700 * screenScaleFactor;
height: 500 * screenScaleFactor;
minimumWidth: 400 * screenScaleFactor;
minimumHeight: 250 * screenScaleFactor;
Item
{
UM.I18nCatalog{id: catalog; name:"cura"}
id: base
property int columnWidth: Math.floor((base.width / 2) - UM.Theme.getSize("default_margin").width)
property int textMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
property string activeScriptName
SystemPalette{ id: palette }
SystemPalette{ id: disabledPalette; colorGroup: SystemPalette.Disabled }
anchors.fill: parent
ExclusiveGroup
{
id: selectedScriptGroup
}
Item
{
id: activeScripts
anchors.left: parent.left
width: base.columnWidth
height: parent.height
Label
{
id: activeScriptsHeader
text: catalog.i18nc("@label", "Post Processing Scripts")
anchors.top: parent.top
anchors.topMargin: base.textMargin
anchors.left: parent.left
anchors.leftMargin: base.textMargin
anchors.right: parent.right
anchors.rightMargin: base.textMargin
font: UM.Theme.getFont("large")
}
ListView
{
id: activeScriptsList
anchors.top: activeScriptsHeader.bottom
anchors.topMargin: base.textMargin
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: base.textMargin
height: childrenRect.height
model: manager.scriptList
delegate: Item
{
width: parent.width
height: activeScriptButton.height
Button
{
id: activeScriptButton
text: manager.getScriptLabelByKey(modelData.toString())
exclusiveGroup: selectedScriptGroup
checkable: true
checked: {
if (manager.selectedScriptIndex == index)
{
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
return true
}
else
{
return false
}
}
onClicked:
{
forceActiveFocus()
manager.setSelectedScriptIndex(index)
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
}
width: parent.width
height: UM.Theme.getSize("setting").height
style: ButtonStyle
{
background: Rectangle
{
color: activeScriptButton.checked ? palette.highlight : "transparent"
width: parent.width
height: parent.height
}
label: Label
{
wrapMode: Text.Wrap
text: control.text
color: activeScriptButton.checked ? palette.highlightedText : palette.text
}
}
}
Button
{
id: removeButton
text: "x"
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
anchors.right:parent.right
anchors.rightMargin: base.textMargin
anchors.verticalCenter: parent.verticalCenter
onClicked: manager.removeScriptByIndex(index)
style: ButtonStyle
{
label: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.7)
height: Math.floor(control.height / 2.7)
sourceSize.width: width
sourceSize.height: width
color: palette.text
source: UM.Theme.getIcon("cross1")
}
}
}
}
Button
{
id: downButton
text: ""
anchors.right: removeButton.left
anchors.verticalCenter: parent.verticalCenter
enabled: index != manager.scriptList.length - 1
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
onClicked:
{
if (manager.selectedScriptIndex == index)
{
manager.setSelectedScriptIndex(index + 1)
}
return manager.moveScript(index, index + 1)
}
style: ButtonStyle
{
label: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.5)
height: Math.floor(control.height / 2.5)
sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
source: UM.Theme.getIcon("arrow_bottom")
}
}
}
}
Button
{
id: upButton
text: ""
enabled: index != 0
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
anchors.right: downButton.left
anchors.verticalCenter: parent.verticalCenter
onClicked:
{
if (manager.selectedScriptIndex == index)
{
manager.setSelectedScriptIndex(index - 1)
}
return manager.moveScript(index, index - 1)
}
style: ButtonStyle
{
label: Item
{
UM.RecolorImage
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.5)
height: Math.floor(control.height / 2.5)
sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
source: UM.Theme.getIcon("arrow_top")
}
}
}
}
}
}
Button
{
id: addButton
text: catalog.i18nc("@action", "Add a script")
anchors.left: parent.left
anchors.leftMargin: base.textMargin
anchors.top: activeScriptsList.bottom
anchors.topMargin: base.textMargin
menu: scriptsMenu
style: ButtonStyle
{
label: Label
{
text: control.text
}
}
}
Menu
{
id: scriptsMenu
Instantiator
{
model: manager.loadedScriptList
MenuItem
{
text: manager.getScriptLabelByKey(modelData.toString())
onTriggered: manager.addScriptToList(modelData.toString())
}
onObjectAdded: scriptsMenu.insertItem(index, object);
onObjectRemoved: scriptsMenu.removeItem(object);
}
}
}
Rectangle
{
color: UM.Theme.getColor("sidebar")
anchors.left: activeScripts.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
height: parent.height
id: settingsPanel
Label
{
id: scriptSpecsHeader
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
anchors.top: parent.top
anchors.topMargin: base.textMargin
anchors.left: parent.left
anchors.leftMargin: base.textMargin
anchors.right: parent.right
anchors.rightMargin: base.textMargin
height: 20 * screenScaleFactor
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
ScrollView
{
id: scrollView
anchors.top: scriptSpecsHeader.bottom
anchors.topMargin: settingsPanel.textMargin
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: manager.selectedScriptDefinitionId != ""
style: UM.Theme.styles.scrollview;
ListView
{
id: listview
spacing: UM.Theme.getSize("default_lining").height
model: UM.SettingDefinitionsModel
{
id: definitionsModel;
containerId: manager.selectedScriptDefinitionId
showAll: true
}
delegate:Loader
{
id: settingLoader
width: parent.width
height:
{
if(provider.properties.enabled == "True")
{
if(model.type != undefined)
{
return UM.Theme.getSize("section").height;
}
else
{
return 0;
}
}
else
{
return 0;
}
}
Behavior on height { NumberAnimation { duration: 100 } }
opacity: provider.properties.enabled == "True" ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 100 } }
enabled: opacity > 0
property var definition: model
property var settingDefinitionsModel: definitionsModel
property var propertyProvider: provider
property var globalPropertyProvider: inheritStackProvider
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
asynchronous: model.type != "enum" && model.type != "extruder"
onLoaded: {
settingLoader.item.showRevertButton = false
settingLoader.item.showInheritButton = false
settingLoader.item.showLinkedSettingIcon = false
settingLoader.item.doDepthIndentation = true
settingLoader.item.doQualityUserSettingEmphasis = false
}
sourceComponent:
{
switch(model.type)
{
case "int":
return settingTextField
case "float":
return settingTextField
case "enum":
return settingComboBox
case "extruder":
return settingExtruder
case "bool":
return settingCheckBox
case "str":
return settingTextField
case "category":
return settingCategory
default:
return settingUnknown
}
}
UM.SettingPropertyProvider
{
id: provider
containerStackId: manager.selectedScriptStackId
key: model.key ? model.key : "None"
watchedProperties: [ "value", "enabled", "state", "validationState" ]
storeIndex: 0
}
// Specialty provider that only watches global_inherits (we cant filter on what property changed we get events
// so we bypass that to make a dedicated provider).
UM.SettingPropertyProvider
{
id: inheritStackProvider
containerStackId: Cura.MachineManager.activeMachineId
key: model.key ? model.key : "None"
watchedProperties: [ "limit_to_extruder" ]
}
Connections
{
target: item
onShowTooltip:
{
tooltip.text = text;
var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0);
tooltip.show(position);
tooltip.target.x = position.x + 1
}
onHideTooltip:
{
tooltip.hide();
}
}
}
}
}
}
Cura.SidebarTooltip
{
id: tooltip
}
Component
{
id: settingTextField;
Cura.SettingTextField { }
}
Component
{
id: settingComboBox;
Cura.SettingComboBox { }
}
Component
{
id: settingExtruder;
Cura.SettingExtruder { }
}
Component
{
id: settingCheckBox;
Cura.SettingCheckBox { }
}
Component
{
id: settingCategory;
Cura.SettingCategory { }
}
Component
{
id: settingUnknown;
Cura.SettingUnknown { }
}
}
rightButtons: Button
{
text: catalog.i18nc("@action:button", "Close")
iconName: "dialog-close"
onClicked: dialog.accept()
}
Button {
objectName: "postProcessingSaveAreaButton"
visible: activeScriptsList.count > 0
height: UM.Theme.getSize("save_button_save_to_button").height
width: height
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
onClicked: dialog.show()
style: ButtonStyle {
background: Rectangle {
id: deviceSelectionIcon
border.width: UM.Theme.getSize("default_lining").width
border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") :
control.pressed ? UM.Theme.getColor("action_button_active_border") :
control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
color: !control.enabled ? UM.Theme.getColor("action_button_disabled") :
control.pressed ? UM.Theme.getColor("action_button_active") :
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
Behavior on color { ColorAnimation { duration: 50; } }
anchors.left: parent.left
anchors.leftMargin: Math.floor(UM.Theme.getSize("save_button_text_margin").width / 2);
width: parent.height
height: parent.height
UM.RecolorImage {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(parent.width / 2)
height: Math.floor(parent.height / 2)
sourceSize.width: width
sourceSize.height: height
color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :
control.pressed ? UM.Theme.getColor("action_button_active_text") :
control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text");
source: "postprocessing.svg"
}
}
label: Label{ }
}
}
}

View file

@ -0,0 +1,2 @@
# PostProcessingPlugin
A post processing plugin for Cura

View file

@ -0,0 +1,111 @@
# Copyright (c) 2015 Jaime van Kessel
# Copyright (c) 2017 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from UM.Logger import Logger
from UM.Signal import Signal, signalemitter
from UM.i18n import i18nCatalog
# Setting stuff import
from UM.Application import Application
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
import re
import json
import collections
i18n_catalog = i18nCatalog("cura")
## Base class for scripts. All scripts should inherit the script class.
@signalemitter
class Script:
def __init__(self):
super().__init__()
self._settings = None
self._stack = None
setting_data = self.getSettingData()
self._stack = ContainerStack(stack_id = str(id(self)))
self._stack.setDirty(False) # This stack does not need to be saved.
## Check if the definition of this script already exists. If not, add it to the registry.
if "key" in setting_data:
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"])
if definitions:
# Definition was found
self._definition = definitions[0]
else:
self._definition = DefinitionContainer(setting_data["key"])
self._definition.deserialize(json.dumps(setting_data))
ContainerRegistry.getInstance().addContainer(self._definition)
self._stack.addContainer(self._definition)
self._instance = InstanceContainer(container_id="ScriptInstanceContainer")
self._instance.setDefinition(self._definition.getId())
self._instance.addMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0))
self._stack.addContainer(self._instance)
self._stack.propertyChanged.connect(self._onPropertyChanged)
ContainerRegistry.getInstance().addContainer(self._stack)
settingsLoaded = Signal()
valueChanged = Signal() # Signal emitted whenever a value of a setting is changed
def _onPropertyChanged(self, key, property_name):
if property_name == "value":
self.valueChanged.emit()
# Property changed: trigger reslice
# To do this we use the global container stack propertyChanged.
# Reslicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack.propertyChanged.emit(key, property_name)
## Needs to return a dict that can be used to construct a settingcategory file.
# See the example script for an example.
# It follows the same style / guides as the Uranium settings.
# Scripts can either override getSettingData directly, or use getSettingDataString
# to return a string that will be parsed as json. The latter has the benefit over
# returning a dict in that the order of settings is maintained.
def getSettingData(self):
setting_data = self.getSettingDataString()
if type(setting_data) == str:
setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict)
return setting_data
def getSettingDataString(self):
raise NotImplementedError()
def getDefinitionId(self):
if self._stack:
return self._stack.getBottom().getId()
def getStackId(self):
if self._stack:
return self._stack.getId()
## Convenience function that retrieves value of a setting from the stack.
def getSettingValueByKey(self, key):
return self._stack.getProperty(key, "value")
## Convenience function that finds the value in a line of g-code.
# When requesting key = x from line "G1 X100" the value 100 is returned.
def getValue(self, line, key, default = None):
if not key in line or (';' in line and line.find(key) > line.find(';')):
return default
sub_part = line[line.find(key) + 1:]
m = re.search('^-?[0-9]+\.?[0-9]*', sub_part)
if m is None:
return default
try:
return float(m.group(0))
except:
return default
## This is called when the script is executed.
# It gets a list of g-code strings and needs to return a (modified) list.
def execute(self, data):
raise NotImplementedError()

View file

@ -0,0 +1,11 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from . import PostProcessingPlugin
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {}
def register(app):
return {"extension": PostProcessingPlugin.PostProcessingPlugin()}

View file

@ -0,0 +1,8 @@
{
"name": "Post Processing",
"author": "Ultimaker",
"version": "2.2",
"api": 4,
"description": "Extension that allows for user created scripts for post processing",
"catalog": "cura"
}

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="512px"
height="512px"
viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="postprocessing.svg"><metadata
id="metadata9"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs7" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1104"
inkscape:window-height="1006"
id="namedview5"
showgrid="false"
inkscape:zoom="1.3359375"
inkscape:cx="256"
inkscape:cy="256"
inkscape:window-x="701"
inkscape:window-y="121"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><path
d="M 402.15234 0 C 371.74552 4.7369516e-015 345.79114 10.752017 324.21875 32.324219 C 302.57788 53.89652 291.82617 79.851497 291.82617 110.18945 C 291.82617 127.34315 295.26662 143.09419 302.16602 157.44531 L 238.38477 221.20312 C 227.77569 210.95036 218.04331 201.50935 209.66016 193.32422 C 207.33386 190.99792 202.68042 189.48707 198.60938 191.92969 L 191.74609 196.11719 C 165.34252 169.24836 154.17609 158.42965 150.57031 145.40234 C 146.84822 131.79345 150.22148 113.64862 153.71094 106.90234 C 156.61882 101.55183 165.69233 96.550326 173.36914 95.96875 L 183.37109 106.20508 C 185.69739 108.53139 189.30456 108.53139 191.63086 106.20508 L 227.57227 69.681641 C 229.89858 67.355335 229.89858 63.517712 227.57227 61.191406 L 169.53125 2.21875 C 167.20494 -0.10755598 163.48147 -0.10755598 161.27148 2.21875 L 125.33008 38.742188 C 123.00378 41.068494 123.00378 44.906116 125.33008 47.232422 L 129.16992 51.1875 C 129.16992 56.88695 128.35573 65.727167 123.70312 70.496094 C 116.49157 77.823958 102.18413 69.332919 92.878906 75.962891 C 83.689998 82.476548 72.05746 92.944493 64.613281 100.38867 C 57.285417 107.83285 29.138171 137.37722 9.015625 187.16016 C -11.106922 236.94311 4.3632369 283.12 15.296875 295.2168 C 21.11264 301.61414 31.696982 308.12804 29.835938 296.03125 C 27.974892 283.81815 24.951448 241.47942 38.792969 224.14844 C 52.634489 206.81746 70.894726 192.62799 94.623047 191.46484 C 117.42084 190.30169 130.56529 198.09417 160.10938 228.10352 L 156.85156 234.15234 C 154.75788 238.10706 155.92175 243.10728 158.24805 245.43359 C 161.95717 248.74082 172.37305 258.96006 186.52539 273.04297 L 6.9511719 452.54883 C 2.2984329 457.14417 1.1842379e-015 462.71497 0 469.14844 C -1.1842379e-015 475.69681 2.2984329 481.15473 6.9511719 485.51953 L 26.308594 505.22266 C 31.018838 509.76054 36.589603 512 42.908203 512 C 49.341623 512 54.800053 509.76054 59.337891 505.22266 L 238.96875 325.6582 C 317.6609 404.95524 424.21289 513.40234 424.21289 513.40234 L 482.25391 454.43164 C 437.71428 411.9686 358.71135 336.76293 291.93164 272.71484 L 354.68945 209.98047 C 369.08663 216.91399 384.90203 220.37891 402.15234 220.37891 C 425.29988 220.37891 446.52947 213.53073 465.77344 199.83398 C 485.08493 186.1372 498.57775 168.33291 506.31641 146.34961 C 510.08303 135.39222 512 126.69334 512 120.25586 C 512 117.79044 511.24662 115.80572 509.87695 114.16211 C 508.50726 112.5185 506.59041 111.69531 504.125 111.69531 C 502.61835 111.69531 496.86414 114.5734 486.72852 120.39453 C 476.6614 126.21564 465.50054 132.85752 453.37891 140.32227 C 441.18878 147.78698 434.7515 151.75888 433.92969 152.23828 L 386.40234 125.94141 L 386.40234 70.8125 L 458.51562 29.242188 C 461.18649 27.461587 462.48633 25.202356 462.48633 22.394531 C 462.48633 19.586706 461.1865 17.325625 458.51562 15.476562 C 451.32484 10.545729 442.4896 6.780346 432.08008 4.0410156 C 421.60206 1.3701797 411.67159 0 402.15234 0 z "
id="path3" /></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,48 @@
from ..Script import Script
class BQ_PauseAtHeight(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Pause at height (BQ Printers)",
"key": "BQ_PauseAtHeight",
"metadata":{},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
}
}
}"""
def execute(self, data):
x = 0.
y = 0.
current_z = 0.
pause_z = self.getSettingValueByKey("pause_height")
for layer in data:
lines = layer.split("\n")
for line in lines:
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
if current_z != None:
if current_z >= pause_z:
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z
# Insert Pause gcode
prepend_gcode += "M25 ; Pauses the print and waits for the user to resume it\n"
index = data.index(layer)
layer = prepend_gcode + layer
data[index] = layer # Override the data of this layer with the modified data
return data
break
return data

View file

@ -0,0 +1,76 @@
# This PostProcessing Plugin script is released
# under the terms of the AGPLv3 or higher
from ..Script import Script
#from UM.Logger import Logger
# from cura.Settings.ExtruderManager import ExtruderManager
class ColorChange(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Color Change",
"key": "ColorChange",
"metadata": {},
"version": 2,
"settings":
{
"layer_number":
{
"label": "Layer",
"description": "At what layer should color change occur. This will be before the layer starts printing. Specify multiple color changes with a comma.",
"unit": "",
"type": "str",
"default_value": "1"
},
"initial_retract":
{
"label": "Initial Retraction",
"description": "Initial filament retraction distance",
"unit": "mm",
"type": "float",
"default_value": 300.0
},
"later_retract":
{
"label": "Later Retraction Distance",
"description": "Later filament retraction distance for removal",
"unit": "mm",
"type": "float",
"default_value": 30.0
}
}
}"""
def execute(self, data: list):
"""data is a list. Each index contains a layer"""
layer_nums = self.getSettingValueByKey("layer_number")
initial_retract = self.getSettingValueByKey("initial_retract")
later_retract = self.getSettingValueByKey("later_retract")
color_change = "M600"
if initial_retract is not None and initial_retract > 0.:
color_change = color_change + (" E%.2f" % initial_retract)
if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract)
color_change = color_change + " ; Generated by ColorChange plugin"
layer_targets = layer_nums.split(',')
if len(layer_targets) > 0:
for layer_num in layer_targets:
layer_num = int( layer_num.strip() )
if layer_num < len(data):
layer = data[ layer_num - 1 ]
lines = layer.split("\n")
lines.insert(2, color_change )
final_line = "\n".join( lines )
data[ layer_num - 1 ] = final_line
return data

View file

@ -0,0 +1,43 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from ..Script import Script
class ExampleScript(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Example script",
"key": "ExampleScript",
"metadata": {},
"version": 2,
"settings":
{
"test":
{
"label": "Test",
"description": "None",
"unit": "mm",
"type": "float",
"default_value": 0.5,
"minimum_value": "0",
"minimum_value_warning": "0.1",
"maximum_value_warning": "1"
},
"derp":
{
"label": "zomg",
"description": "afgasgfgasfgasf",
"unit": "mm",
"type": "float",
"default_value": 0.5,
"minimum_value": "0",
"minimum_value_warning": "0.1",
"maximum_value_warning": "1"
}
}
}"""
def execute(self, data):
return data

View file

@ -0,0 +1,221 @@
from ..Script import Script
# from cura.Settings.ExtruderManager import ExtruderManager
class PauseAtHeight(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Pause at height",
"key": "PauseAtHeight",
"metadata": {},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause Height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_park_x":
{
"label": "Park Print Head X",
"description": "What X location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 190
},
"head_park_y":
{
"label": "Park Print Head Y",
"description": "What Y location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 190
},
"retraction_amount":
{
"label": "Retraction",
"description": "How much filament must be retracted at pause.",
"unit": "mm",
"type": "float",
"default_value": 0
},
"retraction_speed":
{
"label": "Retraction Speed",
"description": "How fast to retract the filament.",
"unit": "mm/s",
"type": "float",
"default_value": 25
},
"extrude_amount":
{
"label": "Extrude Amount",
"description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.",
"unit": "mm",
"type": "float",
"default_value": 0
},
"extrude_speed":
{
"label": "Extrude Speed",
"description": "How fast to extrude the material after pause.",
"unit": "mm/s",
"type": "float",
"default_value": 3.3333
},
"redo_layers":
{
"label": "Redo Layers",
"description": "Redo a number of previous layers after a pause to increases adhesion.",
"unit": "layers",
"type": "int",
"default_value": 0
},
"standby_temperature":
{
"label": "Standby Temperature",
"description": "Change the temperature during the pause",
"unit": "°C",
"type": "int",
"default_value": 0
},
"resume_temperature":
{
"label": "Resume Temperature",
"description": "Change the temperature after the pause",
"unit": "°C",
"type": "int",
"default_value": 0
}
}
}"""
def execute(self, data: list):
"""data is a list. Each index contains a layer"""
x = 0.
y = 0.
current_z = 0.
pause_height = self.getSettingValueByKey("pause_height")
retraction_amount = self.getSettingValueByKey("retraction_amount")
retraction_speed = self.getSettingValueByKey("retraction_speed")
extrude_amount = self.getSettingValueByKey("extrude_amount")
extrude_speed = self.getSettingValueByKey("extrude_speed")
park_x = self.getSettingValueByKey("head_park_x")
park_y = self.getSettingValueByKey("head_park_y")
layers_started = False
redo_layers = self.getSettingValueByKey("redo_layers")
standby_temperature = self.getSettingValueByKey("standby_temperature")
resume_temperature = self.getSettingValueByKey("resume_temperature")
# T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value")
# with open("out.txt", "w") as f:
# f.write(T)
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
layer_0_z = 0.
got_first_g_cmd_on_layer_0 = False
for layer in data:
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
continue
if not layers_started:
continue
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
if not got_first_g_cmd_on_layer_0:
layer_0_z = current_z
got_first_g_cmd_on_layer_0 = True
x = self.getValue(line, 'X', x)
y = self.getValue(line, 'Y', y)
if current_z is not None:
current_height = current_z - layer_0_z
if current_height >= pause_height:
index = data.index(layer)
prevLayer = data[index - 1]
prevLines = prevLayer.split("\n")
current_e = 0.
for prevLine in reversed(prevLines):
current_e = self.getValue(prevLine, 'E', -1)
if current_e >= 0:
break
# include a number of previous layers
for i in range(1, redo_layers + 1):
prevLayer = data[index - i]
layer = prevLayer + layer
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeight.py\n"
prepend_gcode += ";current z: %f \n" % current_z
prepend_gcode += ";current height: %f \n" % current_height
# Retraction
prepend_gcode += "M83\n"
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Move the head away
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
if current_z < 15:
prepend_gcode += "G1 Z15 F300\n"
# Disable the E steppers
prepend_gcode += "M84 E0\n"
# Set extruder standby temperature
prepend_gcode += "M104 S%i; standby temperature\n" % (standby_temperature)
# Wait till the user continues printing
prepend_gcode += "M0 ;Do the actual pause\n"
# Set extruder resume temperature
prepend_gcode += "M109 S%i; resume temperature\n" % (resume_temperature)
# Push the filament back,
if retraction_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Optionally extrude material
if extrude_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (extrude_amount, extrude_speed * 60)
# and retract again, the properly primes the nozzle
# when changing filament.
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Move the head back
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
prepend_gcode += "G1 X%f Y%f F9000\n" % (x, y)
if retraction_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
prepend_gcode += "G1 F9000\n"
prepend_gcode += "M82\n"
# reset extrude value to pre pause value
prepend_gcode += "G92 E%f\n" % (current_e)
layer = prepend_gcode + layer
# Override the data of this layer with the
# modified data
data[index] = layer
return data
break
return data

View file

@ -0,0 +1,169 @@
from ..Script import Script
class PauseAtHeightforRepetier(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Pause at height for repetier",
"key": "PauseAtHeightforRepetier",
"metadata": {},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_park_x":
{
"label": "Park print head X",
"description": "What x location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_park_y":
{
"label": "Park print head Y",
"description": "What y location does the head move to when pausing.",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"head_move_Z":
{
"label": "Head move Z",
"description": "The Hieght of Z-axis retraction before parking.",
"unit": "mm",
"type": "float",
"default_value": 15.0
},
"retraction_amount":
{
"label": "Retraction",
"description": "How much fillament must be retracted at pause.",
"unit": "mm",
"type": "float",
"default_value": 5.0
},
"extrude_amount":
{
"label": "Extrude amount",
"description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.",
"unit": "mm",
"type": "float",
"default_value": 90.0
},
"redo_layers":
{
"label": "Redo layers",
"description": "Redo a number of previous layers after a pause to increases adhesion.",
"unit": "layers",
"type": "int",
"default_value": 0
}
}
}"""
def execute(self, data):
x = 0.
y = 0.
current_z = 0.
pause_z = self.getSettingValueByKey("pause_height")
retraction_amount = self.getSettingValueByKey("retraction_amount")
extrude_amount = self.getSettingValueByKey("extrude_amount")
park_x = self.getSettingValueByKey("head_park_x")
park_y = self.getSettingValueByKey("head_park_y")
move_Z = self.getSettingValueByKey("head_move_Z")
layers_started = False
redo_layers = self.getSettingValueByKey("redo_layers")
for layer in data:
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
continue
if not layers_started:
continue
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
x = self.getValue(line, 'X', x)
y = self.getValue(line, 'Y', y)
if current_z != None:
if current_z >= pause_z:
index = data.index(layer)
prevLayer = data[index-1]
prevLines = prevLayer.split("\n")
current_e = 0.
for prevLine in reversed(prevLines):
current_e = self.getValue(prevLine, 'E', -1)
if current_e >= 0:
break
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeightforRepetier.py\n"
prepend_gcode += ";current z: %f \n" % (current_z)
prepend_gcode += ";current X: %f \n" % (x)
prepend_gcode += ";current Y: %f \n" % (y)
#Retraction
prepend_gcode += "M83\n"
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount)
#Move the head away
prepend_gcode += "G1 Z%f F300\n" % (1 + current_z)
prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
if current_z < move_Z:
prepend_gcode += "G1 Z%f F300\n" % (current_z + move_Z)
#Disable the E steppers
prepend_gcode += "M84 E0\n"
#Wait till the user continues printing
prepend_gcode += "@pause now change filament and press continue printing ;Do the actual pause\n"
#Push the filament back,
if retraction_amount != 0:
prepend_gcode += "G1 E%f F6000\n" % (retraction_amount)
# Optionally extrude material
if extrude_amount != 0:
prepend_gcode += "G1 E%f F200\n" % (extrude_amount)
prepend_gcode += "@info wait for cleaning nozzle from previous filament\n"
prepend_gcode += "@pause remove the waste filament from parking area and press continue printing\n"
# and retract again, the properly primes the nozzle when changing filament.
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount)
#Move the head back
prepend_gcode += "G1 Z%f F300\n" % (1 + current_z)
prepend_gcode +="G1 X%f Y%f F9000\n" % (x, y)
if retraction_amount != 0:
prepend_gcode +="G1 E%f F6000\n" % (retraction_amount)
prepend_gcode +="G1 F9000\n"
prepend_gcode +="M82\n"
# reset extrude value to pre pause value
prepend_gcode +="G92 E%f\n" % (current_e)
layer = prepend_gcode + layer
# include a number of previous layers
for i in range(1, redo_layers + 1):
prevLayer = data[index-i]
layer = prevLayer + layer
data[index] = layer #Override the data of this layer with the modified data
return data
break
return data

View file

@ -0,0 +1,56 @@
# Copyright (c) 2017 Ruben Dulek
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
import re #To perform the search and replace.
from ..Script import Script
## Performs a search-and-replace on all g-code.
#
# Due to technical limitations, the search can't cross the border between
# layers.
class SearchAndReplace(Script):
def getSettingDataString(self):
return """{
"name": "Search and Replace",
"key": "SearchAndReplace",
"metadata": {},
"version": 2,
"settings":
{
"search":
{
"label": "Search",
"description": "All occurrences of this text will get replaced by the replacement text.",
"type": "str",
"default_value": ""
},
"replace":
{
"label": "Replace",
"description": "The search text will get replaced by this text.",
"type": "str",
"default_value": ""
},
"is_regex":
{
"label": "Use Regular Expressions",
"description": "When enabled, the search text will be interpreted as a regular expression.",
"type": "bool",
"default_value": false
}
}
}"""
def execute(self, data):
search_string = self.getSettingValueByKey("search")
if not self.getSettingValueByKey("is_regex"):
search_string = re.escape(search_string) #Need to search for the actual string, not as a regex.
search_regex = re.compile(search_string)
replace_string = self.getSettingValueByKey("replace")
for layer_number, layer in enumerate(data):
data[layer_number] = re.sub(search_regex, replace_string, layer) #Replace all.
return data

View file

@ -0,0 +1,469 @@
# This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher.
"""
Copyright (c) 2017 Christophe Baribaud 2017
Python implementation of https://github.com/electrocbd/post_stretch
Correction of hole sizes, cylinder diameters and curves
See the original description in https://github.com/electrocbd/post_stretch
WARNING This script has never been tested with several extruders
"""
from ..Script import Script
import numpy as np
from UM.Logger import Logger
from UM.Application import Application
import re
def _getValue(line, key, default=None):
"""
Convenience function that finds the value in a line of g-code.
When requesting key = x from line "G1 X100" the value 100 is returned.
It is a copy of Stript's method, so it is no DontRepeatYourself, but
I split the class into setup part (Stretch) and execution part (Strecher)
and only the setup part inherits from Script
"""
if not key in line or (";" in line and line.find(key) > line.find(";")):
return default
sub_part = line[line.find(key) + 1:]
number = re.search(r"^-?[0-9]+\.?[0-9]*", sub_part)
if number is None:
return default
return float(number.group(0))
class GCodeStep():
"""
Class to store the current value of each G_Code parameter
for any G-Code step
"""
def __init__(self, step):
self.step = step
self.step_x = 0
self.step_y = 0
self.step_z = 0
self.step_e = 0
self.step_f = 0
self.comment = ""
def readStep(self, line):
"""
Reads gcode from line into self
"""
self.step_x = _getValue(line, "X", self.step_x)
self.step_y = _getValue(line, "Y", self.step_y)
self.step_z = _getValue(line, "Z", self.step_z)
self.step_e = _getValue(line, "E", self.step_e)
self.step_f = _getValue(line, "F", self.step_f)
return
def copyPosFrom(self, step):
"""
Copies positions of step into self
"""
self.step_x = step.step_x
self.step_y = step.step_y
self.step_z = step.step_z
self.step_e = step.step_e
self.step_f = step.step_f
self.comment = step.comment
return
# Execution part of the stretch plugin
class Stretcher():
"""
Execution part of the stretch algorithm
"""
def __init__(self, line_width, wc_stretch, pw_stretch):
self.line_width = line_width
self.wc_stretch = wc_stretch
self.pw_stretch = pw_stretch
if self.pw_stretch > line_width / 4:
self.pw_stretch = line_width / 4 # Limit value of pushwall stretch distance
self.outpos = GCodeStep(0)
self.vd1 = np.empty((0, 2)) # Start points of segments
# of already deposited material for current layer
self.vd2 = np.empty((0, 2)) # End points of segments
# of already deposited material for current layer
self.layer_z = 0 # Z position of the extrusion moves of the current layer
self.layergcode = ""
def execute(self, data):
"""
Computes the new X and Y coordinates of all g-code steps
"""
Logger.log("d", "Post stretch with line width = " + str(self.line_width)
+ "mm wide circle stretch = " + str(self.wc_stretch)+ "mm"
+ "and push wall stretch = " + str(self.pw_stretch) + "mm")
retdata = []
layer_steps = []
current = GCodeStep(0)
self.layer_z = 0.
current_e = 0.
for layer in data:
lines = layer.rstrip("\n").split("\n")
for line in lines:
current.comment = ""
if line.find(";") >= 0:
current.comment = line[line.find(";"):]
if _getValue(line, "G") == 0:
current.readStep(line)
onestep = GCodeStep(0)
onestep.copyPosFrom(current)
elif _getValue(line, "G") == 1:
current.readStep(line)
onestep = GCodeStep(1)
onestep.copyPosFrom(current)
elif _getValue(line, "G") == 92:
current.readStep(line)
onestep = GCodeStep(-1)
onestep.copyPosFrom(current)
else:
onestep = GCodeStep(-1)
onestep.copyPosFrom(current)
onestep.comment = line
if line.find(";LAYER:") >= 0 and len(layer_steps):
# Previous plugin "forgot" to separate two layers...
Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
+ " " + str(len(layer_steps)) + " steps")
retdata.append(self.processLayer(layer_steps))
layer_steps = []
layer_steps.append(onestep)
# self.layer_z is the z position of the last extrusion move (not travel move)
if current.step_z != self.layer_z and current.step_e != current_e:
self.layer_z = current.step_z
current_e = current.step_e
if len(layer_steps): # Force a new item in the array
Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z)
+ " " + str(len(layer_steps)) + " steps")
retdata.append(self.processLayer(layer_steps))
layer_steps = []
retdata.append(";Wide circle stretch distance " + str(self.wc_stretch) + "\n")
retdata.append(";Push wall stretch distance " + str(self.pw_stretch) + "\n")
return retdata
def extrusionBreak(self, layer_steps, i_pos):
"""
Returns true if the command layer_steps[i_pos] breaks the extruded filament
i.e. it is a travel move
"""
if i_pos == 0:
return True # Begining a layer always breaks filament (for simplicity)
step = layer_steps[i_pos]
prev_step = layer_steps[i_pos - 1]
if step.step_e != prev_step.step_e:
return False
delta_x = step.step_x - prev_step.step_x
delta_y = step.step_y - prev_step.step_y
if delta_x * delta_x + delta_y * delta_y < self.line_width * self.line_width / 4:
# This is a very short movement, less than 0.5 * line_width
# It does not break filament, we should stay in the same extrusion sequence
return False
return True # New sequence
def processLayer(self, layer_steps):
"""
Computes the new coordinates of g-code steps
for one layer (all the steps at the same Z coordinate)
"""
self.outpos.step_x = -1000 # Force output of X and Y coordinates
self.outpos.step_y = -1000 # at each start of layer
self.layergcode = ""
self.vd1 = np.empty((0, 2))
self.vd2 = np.empty((0, 2))
orig_seq = np.empty((0, 2))
modif_seq = np.empty((0, 2))
iflush = 0
for i, step in enumerate(layer_steps):
if step.step == 0 or step.step == 1:
if self.extrusionBreak(layer_steps, i):
# No extrusion since the previous step, so it is a travel move
# Let process steps accumulated into orig_seq,
# which are a sequence of continuous extrusion
modif_seq = np.copy(orig_seq)
if len(orig_seq) >= 2:
self.workOnSequence(orig_seq, modif_seq)
self.generate(layer_steps, iflush, i, modif_seq)
iflush = i
orig_seq = np.empty((0, 2))
orig_seq = np.concatenate([orig_seq, np.array([[step.step_x, step.step_y]])])
if len(orig_seq):
modif_seq = np.copy(orig_seq)
if len(orig_seq) >= 2:
self.workOnSequence(orig_seq, modif_seq)
self.generate(layer_steps, iflush, len(layer_steps), modif_seq)
return self.layergcode
def stepToGcode(self, onestep):
"""
Converts a step into G-Code
For each of the X, Y, Z, E and F parameter,
the parameter is written only if its value changed since the
previous g-code step.
"""
sout = ""
if onestep.step_f != self.outpos.step_f:
self.outpos.step_f = onestep.step_f
sout += " F{:.0f}".format(self.outpos.step_f).rstrip(".")
if onestep.step_x != self.outpos.step_x or onestep.step_y != self.outpos.step_y:
assert onestep.step_x >= -1000 and onestep.step_x < 1000 # If this assertion fails,
# something went really wrong !
self.outpos.step_x = onestep.step_x
sout += " X{:.3f}".format(self.outpos.step_x).rstrip("0").rstrip(".")
assert onestep.step_y >= -1000 and onestep.step_y < 1000 # If this assertion fails,
# something went really wrong !
self.outpos.step_y = onestep.step_y
sout += " Y{:.3f}".format(self.outpos.step_y).rstrip("0").rstrip(".")
if onestep.step_z != self.outpos.step_z or onestep.step_z != self.layer_z:
self.outpos.step_z = onestep.step_z
sout += " Z{:.3f}".format(self.outpos.step_z).rstrip("0").rstrip(".")
if onestep.step_e != self.outpos.step_e:
self.outpos.step_e = onestep.step_e
sout += " E{:.5f}".format(self.outpos.step_e).rstrip("0").rstrip(".")
return sout
def generate(self, layer_steps, ibeg, iend, orig_seq):
"""
Appends g-code lines to the plugin's returned string
starting from step ibeg included and until step iend excluded
"""
ipos = 0
for i in range(ibeg, iend):
if layer_steps[i].step == 0:
layer_steps[i].step_x = orig_seq[ipos][0]
layer_steps[i].step_y = orig_seq[ipos][1]
sout = "G0" + self.stepToGcode(layer_steps[i])
self.layergcode = self.layergcode + sout + "\n"
ipos = ipos + 1
elif layer_steps[i].step == 1:
layer_steps[i].step_x = orig_seq[ipos][0]
layer_steps[i].step_y = orig_seq[ipos][1]
sout = "G1" + self.stepToGcode(layer_steps[i])
self.layergcode = self.layergcode + sout + "\n"
ipos = ipos + 1
else:
self.layergcode = self.layergcode + layer_steps[i].comment + "\n"
def workOnSequence(self, orig_seq, modif_seq):
"""
Computes new coordinates for a sequence
A sequence is a list of consecutive g-code steps
of continuous material extrusion
"""
d_contact = self.line_width / 2.0
if (len(orig_seq) > 2 and
((orig_seq[len(orig_seq) - 1] - orig_seq[0]) ** 2).sum(0) < d_contact * d_contact):
# Starting and ending point of the sequence are nearby
# It is a closed loop
#self.layergcode = self.layergcode + ";wideCircle\n"
self.wideCircle(orig_seq, modif_seq)
else:
#self.layergcode = self.layergcode + ";wideTurn\n"
self.wideTurn(orig_seq, modif_seq) # It is an open curve
if len(orig_seq) > 6: # Don't try push wall on a short sequence
self.pushWall(orig_seq, modif_seq)
if len(orig_seq):
self.vd1 = np.concatenate([self.vd1, np.array(orig_seq[:-1])])
self.vd2 = np.concatenate([self.vd2, np.array(orig_seq[1:])])
def wideCircle(self, orig_seq, modif_seq):
"""
Similar to wideTurn
The first and last point of the sequence are the same,
so it is possible to extend the end of the sequence
with its beginning when seeking for triangles
It is necessary to find the direction of the curve, knowing three points (a triangle)
If the triangle is not wide enough, there is a huge risk of finding
an incorrect orientation, due to insufficient accuracy.
So, when the consecutive points are too close, the method
use following and preceding points to form a wider triangle around
the current point
dmin_tri is the minimum distance between two consecutive points
of an acceptable triangle
"""
dmin_tri = self.line_width / 2.0
iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points
ibeg = 0 # Index of first point of the triangle
iend = 0 # Index of the third point of the triangle
for i, step in enumerate(orig_seq):
if i == 0 or i == len(orig_seq) - 1:
# First and last point of the sequence are the same,
# so it is necessary to skip one of these two points
# when creating a triangle containing the first or the last point
iextra = iextra_base + 1
else:
iextra = iextra_base
# i is the index of the second point of the triangle
# pos_after is the array of positions of the original sequence
# after the current point
pos_after = np.resize(np.roll(orig_seq, -i-1, 0), (iextra, 2))
# Vector of distances between the current point and each following point
dist_from_point = ((step - pos_after) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
iend = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
# pos_before is the array of positions of the original sequence
# before the current point
pos_before = np.resize(np.roll(orig_seq, -i, 0)[::-1], (iextra, 2))
# This time, vector of distances between the current point and each preceding point
dist_from_point = ((step - pos_before) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
ibeg = np.argmax(dist_from_point >= dmin_tri * dmin_tri)
# See https://github.com/electrocbd/post_stretch for explanations
# relpos is the relative position of the projection of the second point
# of the triangle on the segment from the first to the third point
# 0 means the position of the first point, 1 means the position of the third,
# intermediate values are positions between
length_base = ((pos_after[iend] - pos_before[ibeg]) ** 2).sum(0)
relpos = ((step - pos_before[ibeg])
* (pos_after[iend] - pos_before[ibeg])).sum(0)
if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
relpos /= length_base
else:
relpos = 0.5 # To avoid division by zero or precision loss
projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg]))
dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0))
if dist_from_proj > 0.001: # Move central point only if points are not aligned
modif_seq[i] = (step - (self.wc_stretch / dist_from_proj)
* (projection - step))
return
def wideTurn(self, orig_seq, modif_seq):
'''
We have to select three points in order to form a triangle
These three points should be far enough from each other to have
a reliable estimation of the orientation of the current turn
'''
dmin_tri = self.line_width / 2.0
ibeg = 0
iend = 2
for i in range(1, len(orig_seq) - 1):
dist_from_point = ((orig_seq[i] - orig_seq[i+1:]) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
iend = i + 1 + np.argmax(dist_from_point >= dmin_tri * dmin_tri)
dist_from_point = ((orig_seq[i] - orig_seq[i-1::-1]) ** 2).sum(1)
if np.amax(dist_from_point) < dmin_tri * dmin_tri:
continue
ibeg = i - 1 - np.argmax(dist_from_point >= dmin_tri * dmin_tri)
length_base = ((orig_seq[iend] - orig_seq[ibeg]) ** 2).sum(0)
relpos = ((orig_seq[i] - orig_seq[ibeg]) * (orig_seq[iend] - orig_seq[ibeg])).sum(0)
if np.fabs(relpos) < 1000.0 * np.fabs(length_base):
relpos /= length_base
else:
relpos = 0.5
projection = orig_seq[ibeg] + relpos * (orig_seq[iend] - orig_seq[ibeg])
dist_from_proj = np.sqrt(((projection - orig_seq[i]) ** 2).sum(0))
if dist_from_proj > 0.001:
modif_seq[i] = (orig_seq[i] - (self.wc_stretch / dist_from_proj)
* (projection - orig_seq[i]))
return
def pushWall(self, orig_seq, modif_seq):
"""
The algorithm tests for each segment if material was
already deposited at one or the other side of this segment.
If material was deposited at one side but not both,
the segment is moved into the direction of the deposited material,
to "push the wall"
Already deposited material is stored as segments.
vd1 is the array of the starting points of the segments
vd2 is the array of the ending points of the segments
For example, segment nr 8 starts at position self.vd1[8]
and ends at position self.vd2[8]
"""
dist_palp = self.line_width # Palpation distance to seek for a wall
mrot = np.array([[0, -1], [1, 0]]) # Rotation matrix for a quarter turn
for i in range(len(orig_seq)):
ibeg = i # Index of the first point of the segment
iend = i + 1 # Index of the last point of the segment
if iend == len(orig_seq):
iend = i - 1
xperp = np.dot(mrot, orig_seq[iend] - orig_seq[ibeg])
xperp = xperp / np.sqrt((xperp ** 2).sum(-1))
testleft = orig_seq[ibeg] + xperp * dist_palp
materialleft = False # Is there already extruded material at the left of the segment
testright = orig_seq[ibeg] - xperp * dist_palp
materialright = False # Is there already extruded material at the right of the segment
if self.vd1.shape[0]:
relpos = np.clip(((testleft - self.vd1) * (self.vd2 - self.vd1)).sum(1)
/ ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
# nearpoints is the array of the nearest points of each segment
# from the point testleft
dist = ((testleft - nearpoints) * (testleft - nearpoints)).sum(1)
# dist is the array of the squares of the distances between testleft
# and each segment
if np.amin(dist) <= dist_palp * dist_palp:
materialleft = True
# Now the same computation with the point testright at the other side of the
# current segment
relpos = np.clip(((testright - self.vd1) * (self.vd2 - self.vd1)).sum(1)
/ ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.)
nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1)
dist = ((testright - nearpoints) * (testright - nearpoints)).sum(1)
if np.amin(dist) <= dist_palp * dist_palp:
materialright = True
if materialleft and not materialright:
modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch
elif not materialleft and materialright:
modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch
if materialleft and materialright:
modif_seq[ibeg] = orig_seq[ibeg] # Surrounded by walls, don't move
# Setup part of the stretch plugin
class Stretch(Script):
"""
Setup part of the stretch algorithm
The only parameter is the stretch distance
"""
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Post stretch script",
"key": "Stretch",
"metadata": {},
"version": 2,
"settings":
{
"wc_stretch":
{
"label": "Wide circle stretch distance",
"description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect",
"unit": "mm",
"type": "float",
"default_value": 0.08,
"minimum_value": 0,
"minimum_value_warning": 0,
"maximum_value_warning": 0.2
},
"pw_stretch":
{
"label": "Push Wall stretch distance",
"description": "Distance by which the points are moved by the correction effect when two lines are nearby. The higher this value, the higher the effect",
"unit": "mm",
"type": "float",
"default_value": 0.08,
"minimum_value": 0,
"minimum_value_warning": 0,
"maximum_value_warning": 0.2
}
}
}"""
def execute(self, data):
"""
Entry point of the plugin.
data is the list of original g-code instructions,
the returned string is the list of modified g-code instructions
"""
stretcher = Stretcher(
Application.getInstance().getGlobalContainerStack().getProperty("line_width", "value")
, self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch"))
return stretcher.execute(data)

View file

@ -0,0 +1,495 @@
# TweakAtZ script - Change printing parameters at a given height
# This script is the successor of the TweakAtZ plugin for legacy Cura.
# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
#Authors of the TweakAtZ plugin / script:
# Written by Steven Morlock, smorloc@gmail.com
# Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+
# Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below)
# Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x
# Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug.
##history / changelog:
##V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment)
##V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at,
## extruder three temperature disabled by "#Ex3"
##V3.1.1: Bugfix reset flow rate
##V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift
##V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser,
## added speed reset at the end of the print
##V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option,
## extruder three code removed, tweaking print speed, save call of Publisher class,
## uses previous value from other plugins also on UltiGCode
##V4.0.1: Bugfix for doubled G1 commands
##V4.0.2: uses Cura progress bar instead of its own
##V4.0.3: Bugfix for cool head lift (contributed by luisonoff)
##V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin
##V4.9.92: Modifications for Cura 15.10
##V4.9.93: Minor bugfixes (input settings) / documentation
##V4.9.94: Bugfix Combobox-selection; remove logger
##V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x
##V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed'
##V5.1: API Changes included for use with Cura 2.2
## Uses -
## M220 S<factor in percent> - set speed factor override percentage
## M221 S<factor in percent> - set flow factor override percentage
## M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
## M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
## M140 S<temp> - set bed target temperature
## M106 S<PWM> - set fan speed to target speed <S>
## M605/606 to save and recall material settings on the UM2
from ..Script import Script
#from UM.Logger import Logger
import re
class TweakAtZ(Script):
version = "5.1.1"
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"TweakAtZ """ + self.version + """ (Experimental)",
"key":"TweakAtZ",
"metadata": {},
"version": 2,
"settings":
{
"a_trigger":
{
"label": "Trigger",
"description": "Trigger at height or at layer no.",
"type": "enum",
"options": {"height":"Height","layer_no":"Layer No."},
"default_value": "height"
},
"b_targetZ":
{
"label": "Tweak Height",
"description": "Z height to tweak at",
"unit": "mm",
"type": "float",
"default_value": 5.0,
"minimum_value": "0",
"minimum_value_warning": "0.1",
"maximum_value_warning": "230",
"enabled": "a_trigger == 'height'"
},
"b_targetL":
{
"label": "Tweak Layer",
"description": "Layer no. to tweak at",
"unit": "",
"type": "int",
"default_value": 1,
"minimum_value": "-100",
"minimum_value_warning": "-1",
"enabled": "a_trigger == 'layer_no'"
},
"c_behavior":
{
"label": "Behavior",
"description": "Select behavior: Tweak value and keep it for the rest, Tweak value for single layer only",
"type": "enum",
"options": {"keep_value":"Keep value","single_layer":"Single Layer"},
"default_value": "keep_value"
},
"d_twLayers":
{
"label": "No. Layers",
"description": "No. of layers used to tweak",
"unit": "",
"type": "int",
"default_value": 1,
"minimum_value": "1",
"maximum_value_warning": "50",
"enabled": "c_behavior == 'keep_value'"
},
"e1_Tweak_speed":
{
"label": "Tweak Speed",
"description": "Select if total speed (print and travel) has to be tweaked",
"type": "bool",
"default_value": false
},
"e2_speed":
{
"label": "Speed",
"description": "New total speed (print and travel)",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "e1_Tweak_speed"
},
"f1_Tweak_printspeed":
{
"label": "Tweak Print Speed",
"description": "Select if print speed has to be tweaked",
"type": "bool",
"default_value": false
},
"f2_printspeed":
{
"label": "Print Speed",
"description": "New print speed",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "f1_Tweak_printspeed"
},
"g1_Tweak_flowrate":
{
"label": "Tweak Flow Rate",
"description": "Select if flow rate has to be tweaked",
"type": "bool",
"default_value": false
},
"g2_flowrate":
{
"label": "Flow Rate",
"description": "New Flow rate",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g1_Tweak_flowrate"
},
"g3_Tweak_flowrateOne":
{
"label": "Tweak Flow Rate 1",
"description": "Select if first extruder flow rate has to be tweaked",
"type": "bool",
"default_value": false
},
"g4_flowrateOne":
{
"label": "Flow Rate One",
"description": "New Flow rate Extruder 1",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g3_Tweak_flowrateOne"
},
"g5_Tweak_flowrateTwo":
{
"label": "Tweak Flow Rate 2",
"description": "Select if second extruder flow rate has to be tweaked",
"type": "bool",
"default_value": false
},
"g6_flowrateTwo":
{
"label": "Flow Rate two",
"description": "New Flow rate Extruder 2",
"unit": "%",
"type": "int",
"default_value": 100,
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g5_Tweak_flowrateTwo"
},
"h1_Tweak_bedTemp":
{
"label": "Tweak Bed Temp",
"description": "Select if Bed Temperature has to be tweaked",
"type": "bool",
"default_value": false
},
"h2_bedTemp":
{
"label": "Bed Temp",
"description": "New Bed Temperature",
"unit": "C",
"type": "float",
"default_value": 60,
"minimum_value": "0",
"minimum_value_warning": "30",
"maximum_value_warning": "120",
"enabled": "h1_Tweak_bedTemp"
},
"i1_Tweak_extruderOne":
{
"label": "Tweak Extruder 1 Temp",
"description": "Select if First Extruder Temperature has to be tweaked",
"type": "bool",
"default_value": false
},
"i2_extruderOne":
{
"label": "Extruder 1 Temp",
"description": "New First Extruder Temperature",
"unit": "C",
"type": "float",
"default_value": 190,
"minimum_value": "0",
"minimum_value_warning": "160",
"maximum_value_warning": "250",
"enabled": "i1_Tweak_extruderOne"
},
"i3_Tweak_extruderTwo":
{
"label": "Tweak Extruder 2 Temp",
"description": "Select if Second Extruder Temperature has to be tweaked",
"type": "bool",
"default_value": false
},
"i4_extruderTwo":
{
"label": "Extruder 2 Temp",
"description": "New Second Extruder Temperature",
"unit": "C",
"type": "float",
"default_value": 190,
"minimum_value": "0",
"minimum_value_warning": "160",
"maximum_value_warning": "250",
"enabled": "i3_Tweak_extruderTwo"
},
"j1_Tweak_fanSpeed":
{
"label": "Tweak Fan Speed",
"description": "Select if Fan Speed has to be tweaked",
"type": "bool",
"default_value": false
},
"j2_fanSpeed":
{
"label": "Fan Speed",
"description": "New Fan Speed (0-255)",
"unit": "PWM",
"type": "int",
"default_value": 255,
"minimum_value": "0",
"minimum_value_warning": "15",
"maximum_value_warning": "255",
"enabled": "j1_Tweak_fanSpeed"
}
}
}"""
def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature
if not key in line or (";" in line and line.find(key) > line.find(";") and
not ";TweakAtZ" in key and not ";LAYER:" in key):
return default
subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1
if ";TweakAtZ" in key:
m = re.search("^[0-4]", subPart)
elif ";LAYER:" in key:
m = re.search("^[+-]?[0-9]*", subPart)
else:
#the minus at the beginning allows for negative values, e.g. for delta printers
m = re.search("^[-]?[0-9]*\.?[0-9]*", subPart)
if m == None:
return default
try:
return float(m.group(0))
except:
return default
def execute(self, data):
#Check which tweaks should apply
TweakProp = {"speed": self.getSettingValueByKey("e1_Tweak_speed"),
"flowrate": self.getSettingValueByKey("g1_Tweak_flowrate"),
"flowrateOne": self.getSettingValueByKey("g3_Tweak_flowrateOne"),
"flowrateTwo": self.getSettingValueByKey("g5_Tweak_flowrateTwo"),
"bedTemp": self.getSettingValueByKey("h1_Tweak_bedTemp"),
"extruderOne": self.getSettingValueByKey("i1_Tweak_extruderOne"),
"extruderTwo": self.getSettingValueByKey("i3_Tweak_extruderTwo"),
"fanSpeed": self.getSettingValueByKey("j1_Tweak_fanSpeed")}
TweakPrintSpeed = self.getSettingValueByKey("f1_Tweak_printspeed")
TweakStrings = {"speed": "M220 S%f\n",
"flowrate": "M221 S%f\n",
"flowrateOne": "M221 T0 S%f\n",
"flowrateTwo": "M221 T1 S%f\n",
"bedTemp": "M140 S%f\n",
"extruderOne": "M104 S%f T0\n",
"extruderTwo": "M104 S%f T1\n",
"fanSpeed": "M106 S%d\n"}
target_values = {"speed": self.getSettingValueByKey("e2_speed"),
"printspeed": self.getSettingValueByKey("f2_printspeed"),
"flowrate": self.getSettingValueByKey("g2_flowrate"),
"flowrateOne": self.getSettingValueByKey("g4_flowrateOne"),
"flowrateTwo": self.getSettingValueByKey("g6_flowrateTwo"),
"bedTemp": self.getSettingValueByKey("h2_bedTemp"),
"extruderOne": self.getSettingValueByKey("i2_extruderOne"),
"extruderTwo": self.getSettingValueByKey("i4_extruderTwo"),
"fanSpeed": self.getSettingValueByKey("j2_fanSpeed")}
old = {"speed": -1, "flowrate": -1, "flowrateOne": -1, "flowrateTwo": -1, "platformTemp": -1, "extruderOne": -1,
"extruderTwo": -1, "bedTemp": -1, "fanSpeed": -1, "state": -1}
twLayers = self.getSettingValueByKey("d_twLayers")
if self.getSettingValueByKey("c_behavior") == "single_layer":
behavior = 1
else:
behavior = 0
try:
twLayers = max(int(twLayers),1) #for the case someone entered something as "funny" as -1
except:
twLayers = 1
pres_ext = 0
done_layers = 0
z = 0
x = None
y = None
layer = -100000 #layer no. may be negative (raft) but never that low
# state 0: deactivated, state 1: activated, state 2: active, but below z,
# state 3: active and partially executed (multi layer), state 4: active and passed z
state = 1
# IsUM2: Used for reset of values (ok for Marlin/Sprinter),
# has to be set to 1 for UltiGCode (work-around for missing default values)
IsUM2 = False
oldValueUnknown = False
TWinstances = 0
if self.getSettingValueByKey("a_trigger") == "layer_no":
targetL_i = int(self.getSettingValueByKey("b_targetL"))
targetZ = 100000
else:
targetL_i = -100000
targetZ = self.getSettingValueByKey("b_targetZ")
index = 0
for active_layer in data:
modified_gcode = ""
lines = active_layer.split("\n")
for line in lines:
if ";Generated with Cura_SteamEngine" in line:
TWinstances += 1
modified_gcode += ";TweakAtZ instances: %d\n" % TWinstances
if not ("M84" in line or "M25" in line or ("G1" in line and TweakPrintSpeed and (state==3 or state==4)) or
";TweakAtZ instances:" in line):
modified_gcode += line + "\n"
IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode!
if ";TweakAtZ-state" in line: #checks for state change comment
state = self.getValue(line, ";TweakAtZ-state", state)
if ";TweakAtZ instances:" in line:
try:
tempTWi = int(line[20:])
except:
tempTWi = TWinstances
TWinstances = tempTWi
if ";Small layer" in line: #checks for begin of Cool Head Lift
old["state"] = state
state = 0
if ";LAYER:" in line: #new layer no. found
if state == 0:
state = old["state"]
layer = self.getValue(line, ";LAYER:", layer)
if targetL_i > -100000: #target selected by layer no.
if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for tweak on layer 0
state = 2
targetZ = z + 0.001
if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd
pres_ext = self.getValue(line, "T", pres_ext)
if "M190" in line or "M140" in line and state < 3: #looking for bed temp, stops after target z is passed
old["bedTemp"] = self.getValue(line, "S", old["bedTemp"])
if "M109" in line or "M104" in line and state < 3: #looking for extruder temp, stops after target z is passed
if self.getValue(line, "T", pres_ext) == 0:
old["extruderOne"] = self.getValue(line, "S", old["extruderOne"])
elif self.getValue(line, "T", pres_ext) == 1:
old["extruderTwo"] = self.getValue(line, "S", old["extruderTwo"])
if "M107" in line: #fan is stopped; is always updated in order not to miss switch off for next object
old["fanSpeed"] = 0
if "M106" in line and state < 3: #looking for fan speed
old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"])
if "M221" in line and state < 3: #looking for flow rate
tmp_extruder = self.getValue(line,"T",None)
if tmp_extruder == None: #check if extruder is specified
old["flowrate"] = self.getValue(line, "S", old["flowrate"])
elif tmp_extruder == 0: #first extruder
old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
elif tmp_extruder == 1: #second extruder
old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
if ("M84" in line or "M25" in line):
if state>0 and TweakProp["speed"]: #"finish" commands for UM Original and UM2
modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n"
modified_gcode += "M117 \n"
modified_gcode += line + "\n"
if "G1" in line or "G0" in line:
newZ = self.getValue(line, "Z", z)
x = self.getValue(line, "X", None)
y = self.getValue(line, "Y", None)
e = self.getValue(line, "E", None)
f = self.getValue(line, "F", None)
if 'G1' in line and TweakPrintSpeed and (state==3 or state==4):
# check for pure print movement in target range:
if x != None and y != None and f != None and e != None and newZ==z:
modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"),
self.getValue(line, "Y"), self.getValue(line, "E"))
else: #G1 command but not a print movement
modified_gcode += line + "\n"
# no tweaking on retraction hops which have no x and y coordinate:
if (newZ != z) and (x is not None) and (y is not None):
z = newZ
if z < targetZ and state == 1:
state = 2
if z >= targetZ and state == 2:
state = 3
done_layers = 0
for key in TweakProp:
if TweakProp[key] and old[key]==-1: #old value is not known
oldValueUnknown = True
if oldValueUnknown: #the tweaking has to happen within one layer
twLayers = 1
if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2)
modified_gcode += "M605 S%d;stores parameters before tweaking\n" % (TWinstances-1)
if behavior == 1: #single layer tweak only and then reset
twLayers = 1
if TweakPrintSpeed and behavior == 0:
twLayers = done_layers + 1
if state==3:
if twLayers-done_layers>0: #still layers to go?
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: executed at Layer %d\n" % (self.version,layer)
modified_gcode += "M117 Printing... tw@L%4d\n" % layer
else:
modified_gcode += (";TweakAtZ V%s: executed at %1.2f mm\n" % (self.version,z))
modified_gcode += "M117 Printing... tw@%5.1f\n" % z
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1))
done_layers += 1
else:
state = 4
if behavior == 1: #reset values after one layer
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: reset on Layer %d\n" % (self.version,layer)
else:
modified_gcode += ";TweakAtZ V%s: reset at %1.2f mm\n" % (self.version,z)
if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
else: #executes on RepRap, UM2 with Ultigcode and Cura setting
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key])
# re-activates the plugin if executed by pre-print G-command, resets settings:
if (z < targetZ or layer == 0) and state >= 3: #resets if below tweak level or at level 0
state = 2
done_layers = 0
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i)
else:
modified_gcode += ";TweakAtZ V%s: reset below %1.2f mm\n" % (self.version,targetZ)
if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
else: #executes on RepRap, UM2 with Ultigcode and Cura setting
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key])
data[index] = modified_gcode
index += 1
return data

View file

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

View file

@ -104,7 +104,7 @@ class SimulationView(View):
title = catalog.i18nc("@info:title", "Simulation View"))
def _resetSettings(self):
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
self._extruder_count = 0
self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
self._show_travel_moves = 0

View file

@ -176,7 +176,6 @@ Item
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
}
}
Label

View file

@ -28,6 +28,7 @@ class SolidView(View):
self._enabled_shader = None
self._disabled_shader = None
self._non_printing_shader = None
self._support_mesh_shader = None
self._extruders_model = ExtrudersModel()
self._theme = None
@ -54,6 +55,11 @@ class SolidView(View):
self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
if not self._support_mesh_shader:
self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
self._support_mesh_shader.setUniformValue("u_vertical_stripes", True)
self._support_mesh_shader.setUniformValue("u_width", 5.0)
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
@ -117,6 +123,16 @@ class SolidView(View):
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader)
elif per_mesh_stack and per_mesh_stack.getProperty("support_mesh", "value"):
# Render support meshes with a vertical stripe that is darker
shade_factor = 0.6
uniforms["diffuse_color_2"] = [
uniforms["diffuse_color"][0] * shade_factor,
uniforms["diffuse_color"][1] * shade_factor,
uniforms["diffuse_color"][2] * shade_factor,
1.0
]
renderer.queueNode(node, shader = self._support_mesh_shader, uniforms = uniforms)
else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node):

View file

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

View file

@ -12,10 +12,10 @@ Component
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
@ -33,9 +33,9 @@ Component
horizontalCenter: parent.horizontalCenter
}
text: OutputDevice.connectedPrinters.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
visible: OutputDevice.connectedPrinters.length == 0
visible: OutputDevice.printers.length == 0
}
Item
@ -46,7 +46,7 @@ Component
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: children.height
visible: OutputDevice.connectedPrinters.length != 0
visible: OutputDevice.printers.length != 0
Label
{
@ -62,7 +62,6 @@ Component
}
}
ScrollView
{
id: printerScrollView
@ -79,7 +78,7 @@ Component
anchors.fill: parent
spacing: -UM.Theme.getSize("default_lining").height
model: OutputDevice.connectedPrinters
model: OutputDevice.printers
delegate: PrinterInfoBlock
{
@ -95,7 +94,7 @@ Component
PrinterVideoStream
{
visible: OutputDevice.selectedPrinterName != ""
visible: OutputDevice.activePrinter != null
anchors.fill:parent
}
}

View file

@ -0,0 +1,435 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from UM.Message import Message
from UM.Qt.Duration import Duration, DurationFormat
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.NetworkCamera import NetworkCamera
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from time import time
from datetime import datetime
from typing import Optional
import json
import os
i18n_catalog = i18nCatalog("cura")
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
printJobsChanged = pyqtSignal()
activePrinterChanged = pyqtSignal()
# This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
# Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
clusterPrintersChanged = pyqtSignal()
def __init__(self, device_id, address, properties, parent = None):
super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
self._api_prefix = "/cluster-api/v1/"
self._number_of_extruders = 2
self._print_jobs = []
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
# See comments about this hack with the clusterPrintersChanged signal
self.printersChanged.connect(self.clusterPrintersChanged)
self._accepts_commands = True
# Cluster does not have authentication, so default to authenticated
self._authentication_state = AuthState.Authenticated
self._error_message = None
self._progress_message = None
self._active_printer = None # type: Optional[PrinterOutputModel]
self._printer_selection_dialog = None
self.setPriority(3) # Make sure the output device gets selected above local file output
self.setName(self._id)
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
self._printer_uuid_to_unique_name_mapping = {}
self._finished_jobs = []
self._cluster_size = int(properties.get(b"cluster_size", 0))
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
# Unable to find g-code. Nothing to send
return
self._gcode = gcode_list
if len(self._printers) > 1:
self._spawnPrinterSelectionDialog()
else:
self.sendPrintJob()
# Notify the UI that a switch to the print monitor should happen
Application.getInstance().getController().setActiveStage("MonitorStage")
def _spawnPrinterSelectionDialog(self):
if self._printer_selection_dialog is None:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml")
self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show()
@pyqtProperty(int, constant=True)
def clusterSize(self):
return self._cluster_size
@pyqtSlot()
@pyqtSlot(str)
def sendPrintJob(self, target_printer = ""):
Logger.log("i", "Sending print job to printer.")
if self._sending_gcode:
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
self._sending_gcode = True
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
i18n_catalog.i18nc("@info:title", "Sending Data"))
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
self._progress_message.show()
compressed_gcode = self._compressGCode()
if compressed_gcode is None:
# Abort was called.
return
parts = []
# If a specific printer was selected, it should be printed with that machine.
if target_printer:
target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))
# Add user name to the print_job
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode))
self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
@pyqtProperty(QObject, notify=activePrinterChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]:
return self._active_printer
@pyqtSlot(QObject)
def setActivePrinter(self, printer):
if self._active_printer != printer:
if self._active_printer and self._active_printer.camera:
self._active_printer.camera.stop()
self._active_printer = printer
self.activePrinterChanged.emit()
def _onPostPrintJobFinished(self, reply):
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
if bytes_total > 0:
new_progress = bytes_sent / bytes_total * 100
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
# timeout responses if this happens.
self._last_response_time = time()
if new_progress > self._progress_message.getProgress():
self._progress_message.show() # Ensure that the message is visible.
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
else:
self._progress_message.setProgress(0)
self._progress_message.hide()
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
if action_id == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
@pyqtSlot()
def openPrintJobControlPanel(self):
Logger.log("d", "Opening print job control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
@pyqtSlot()
def openPrinterControlPanel(self):
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self):
return self._print_jobs
@pyqtProperty("QVariantList", notify=printJobsChanged)
def queuedPrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None]
@pyqtProperty("QVariantList", notify=printJobsChanged)
def activePrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None]
@pyqtProperty("QVariantList", notify=clusterPrintersChanged)
def connectedPrintersTypeCount(self):
printer_count = {}
for printer in self._printers:
if printer.type in printer_count:
printer_count[printer.type] += 1
else:
printer_count[printer.type] = 1
result = []
for machine_type in printer_count:
result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
return result
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
@pyqtSlot(int, result=str)
def getTimeCompleted(self, time_remaining):
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
@pyqtSlot(int, result=str)
def getDateCompleted(self, time_remaining):
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
def _printJobStateChanged(self):
username = self._getUserName()
if username is None:
return # We only want to show notifications if username is set.
finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]
newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
for job in newly_finished_jobs:
job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
job_completed_message.show()
# Ensure UI gets updated
self.printJobsChanged.emit()
# Keep a list of all completed jobs so we know if something changed next time.
self._finished_jobs = finished_jobs
def _update(self):
if not super()._update():
return
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
def _onGetPrintJobsFinished(self, reply: QNetworkReply):
if not checkValidGetReply(reply):
return
result = loadJsonFromReply(reply)
if result is None:
return
print_jobs_seen = []
job_list_changed = False
for print_job_data in result:
print_job = findByKey(self._print_jobs, print_job_data["uuid"])
if print_job is None:
print_job = self._createPrintJobModel(print_job_data)
job_list_changed = True
self._updatePrintJob(print_job, print_job_data)
if print_job.state != "queued": # Print job should be assigned to a printer.
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
else: # The job can "reserve" a printer if some changes are required.
printer = self._getPrinterByKey(print_job_data["assigned_to"])
if printer:
printer.updateActivePrintJob(print_job)
print_jobs_seen.append(print_job)
# Check what jobs need to be removed.
removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]
for removed_job in removed_jobs:
job_list_changed |= self._removeJob(removed_job)
if job_list_changed:
self.printJobsChanged.emit() # Do a single emit for all print job changes.
def _onGetPrintersDataFinished(self, reply: QNetworkReply):
if not checkValidGetReply(reply):
return
result = loadJsonFromReply(reply)
if result is None:
return
printer_list_changed = False
printers_seen = []
for printer_data in result:
printer = findByKey(self._printers, printer_data["uuid"])
if printer is None:
printer = self._createPrinterModel(printer_data)
printer_list_changed = True
printers_seen.append(printer)
self._updatePrinter(printer, printer_data)
removed_printers = [printer for printer in self._printers if printer not in printers_seen]
for printer in removed_printers:
self._removePrinter(printer)
if removed_printers or printer_list_changed:
self.printersChanged.emit()
def _createPrinterModel(self, data):
printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
number_of_extruders=self._number_of_extruders)
printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
self._printers.append(printer)
return printer
def _createPrintJobModel(self, data):
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
key=data["uuid"], name= data["name"])
print_job.stateChanged.connect(self._printJobStateChanged)
self._print_jobs.append(print_job)
return print_job
def _updatePrintJob(self, print_job, data):
print_job.updateTimeTotal(data["time_total"])
print_job.updateTimeElapsed(data["time_elapsed"])
print_job.updateState(data["status"])
print_job.updateOwner(data["owner"])
def _updatePrinter(self, printer, data):
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
printer.updateName(data["friendly_name"])
printer.updateKey(data["uuid"])
printer.updateType(data["machine_variant"])
if not data["enabled"]:
printer.updateState("disabled")
else:
printer.updateState(data["status"])
for index in range(0, self._number_of_extruders):
extruder = printer.extruders[index]
try:
extruder_data = data["configuration"][index]
except IndexError:
break
extruder.updateHotendID(extruder_data.get("print_core_id", ""))
material_data = extruder_data["material"]
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
GUID=material_data["guid"])
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
Logger.log("w",
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
guid=material_data["guid"]))
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
name = "Unknown"
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
brand=brand, color=color, name=name)
extruder.updateActiveMaterial(material)
def _removeJob(self, job):
if job not in self._print_jobs:
return False
if job.assignedPrinter:
job.assignedPrinter.updateActivePrintJob(None)
job.stateChanged.disconnect(self._printJobStateChanged)
self._print_jobs.remove(job)
return True
def _removePrinter(self, printer):
self._printers.remove(printer)
if self._active_printer == printer:
self._active_printer = None
self.activePrinterChanged.emit()
def loadJsonFromReply(reply):
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.logException("w", "Unable to decode JSON from reply.")
return
return result
def checkValidGetReply(reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code != 200:
Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code))
return False
return True
def findByKey(list, key):
for item in list:
if item.key == key:
return item

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")
class DiscoverUM3Action(MachineAction):
discoveredDevicesChanged = pyqtSignal()
def __init__(self):
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
self._qml_url = "DiscoverUM3Action.qml"
@ -25,24 +28,24 @@ class DiscoverUM3Action(MachineAction):
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
self._last_zeroconf_event_time = time.time()
self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset
self._last_zero_conf_event_time = time.time()
printersChanged = pyqtSignal()
# Time to wait after a zero-conf service change before allowing a zeroconf reset
self._zero_conf_change_grace_period = 0.25
@pyqtSlot()
def startDiscovery(self):
if not self._network_plugin:
Logger.log("d", "Starting printer discovery.")
Logger.log("d", "Starting device discovery.")
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
self.printersChanged.emit()
self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self.discoveredDevicesChanged.emit()
## Re-filters the list of printers.
## Re-filters the list of devices.
@pyqtSlot()
def reset(self):
Logger.log("d", "Reset the list of found printers.")
self.printersChanged.emit()
Logger.log("d", "Reset the list of found devices.")
self.discoveredDevicesChanged.emit()
@pyqtSlot()
def restartDiscovery(self):
@ -51,43 +54,44 @@ class DiscoverUM3Action(MachineAction):
# It's most likely that the QML engine is still creating delegates, where the python side already deleted or
# garbage collected the data.
# Whatever the case, waiting a bit ensures that it doesn't crash.
if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period:
if time.time() - self._last_zero_conf_event_time > self._zero_conf_change_grace_period:
if not self._network_plugin:
self.startDiscovery()
else:
self._network_plugin.startDiscovery()
@pyqtSlot(str, str)
def removeManualPrinter(self, key, address):
def removeManualDevice(self, key, address):
if not self._network_plugin:
return
self._network_plugin.removeManualPrinter(key, address)
self._network_plugin.removeManualDevice(key, address)
@pyqtSlot(str, str)
def setManualPrinter(self, key, address):
def setManualDevice(self, key, address):
if key != "":
# This manual printer replaces a current manual printer
self._network_plugin.removeManualPrinter(key)
self._network_plugin.removeManualDevice(key)
if address != "":
self._network_plugin.addManualPrinter(address)
self._network_plugin.addManualDevice(address)
def _onPrinterDiscoveryChanged(self, *args):
self._last_zeroconf_event_time = time.time()
self.printersChanged.emit()
def _onDeviceDiscoveryChanged(self, *args):
self._last_zero_conf_event_time = time.time()
self.discoveredDevicesChanged.emit()
@pyqtProperty("QVariantList", notify = printersChanged)
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
def foundDevices(self):
if self._network_plugin:
# TODO: Check if this needs to stay.
if Application.getInstance().getGlobalContainerStack():
global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId()
else:
global_printer_type = "unknown"
printers = list(self._network_plugin.getPrinters().values())
printers = list(self._network_plugin.getDiscoveredDevices().values())
# TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet.
printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
#printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
printers.sort(key = lambda k: k.name)
return printers
else:

View file

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

View file

@ -0,0 +1,632 @@
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.NetworkCamera import NetworkCamera
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Application import Application
from UM.i18n import i18nCatalog
from UM.Message import Message
from PyQt5.QtNetwork import QNetworkRequest
from PyQt5.QtCore import QTimer, QCoreApplication
from PyQt5.QtWidgets import QMessageBox
from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController
from time import time
import json
import os
i18n_catalog = i18nCatalog("cura")
## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API.
# Everything after that firmware uses the ClusterUM3Output.
# The Legacy output device can only have one printer (whereas the cluster can have 0 to n).
#
# Authentication is done in a number of steps;
# 1. Request an id / key pair by sending the application & user name. (state = authRequested)
# 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived)
# 3. OutputDevice will poll if the button was pressed.
# 4. At this point the machine either has the state Authenticated or AuthenticationDenied.
# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def __init__(self, device_id, address: str, properties, parent = None):
super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
self._api_prefix = "/api/v1/"
self._number_of_extruders = 2
self._authentication_id = None
self._authentication_key = None
self._authentication_counter = 0
self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min)
self._authentication_timer = QTimer()
self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval
self._authentication_timer.setSingleShot(False)
self._authentication_timer.timeout.connect(self._onAuthenticationTimer)
# The messages are created when connect is called the first time.
# This ensures that the messages are only created for devices that actually want to connect.
self._authentication_requested_message = None
self._authentication_failed_message = None
self._authentication_succeeded_message = None
self._not_authenticated_message = None
self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)
self.setPriority(3) # Make sure the output device gets selected above local file output
self.setName(self._id)
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
self.setIconName("print")
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
self._output_controller = LegacyUM3PrinterOutputController(self)
def _onAuthenticationStateChanged(self):
# We only accept commands if we are authenticated.
if self._authentication_state == AuthState.Authenticated:
self._setAcceptsCommands(True)
else:
self._setAcceptsCommands(False)
def _setupMessages(self):
self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
"Access to the printer requested. Please approve the request on the printer"),
lifetime=0, dismissable=False, progress=0,
title=i18n_catalog.i18nc("@info:title",
"Authentication status"))
self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""),
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
self._authentication_succeeded_message = Message(
i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._not_authenticated_message = Message(
i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
None, i18n_catalog.i18nc("@info:tooltip",
"Send access request to the printer"))
self._not_authenticated_message.actionTriggered.connect(self._messageCallback)
def _messageCallback(self, message_id=None, action_id="Retry"):
if action_id == "Request" or action_id == "Retry":
if self._authentication_failed_message:
self._authentication_failed_message.hide()
if self._not_authenticated_message:
self._not_authenticated_message.hide()
self._requestAuthentication()
def connect(self):
super().connect()
self._setupMessages()
global_container = Application.getInstance().getGlobalContainerStack()
if global_container:
self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)
def close(self):
super().close()
if self._authentication_requested_message:
self._authentication_requested_message.hide()
if self._authentication_failed_message:
self._authentication_failed_message.hide()
if self._authentication_succeeded_message:
self._authentication_succeeded_message.hide()
self._sending_gcode = False
self._compressing_gcode = False
self._authentication_timer.stop()
## Send all material profiles to the printer.
def _sendMaterialProfiles(self):
Logger.log("i", "Sending material profiles to printer")
# TODO: Might want to move this to a job...
for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
try:
xml_data = container.serialize()
if xml_data == "" or xml_data is None:
continue
names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
if names:
# There are other materials that share this GUID.
if not container.isReadOnly():
continue # If it's not readonly, it's created by user, so skip it.
file_name = "none.xml"
self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None)
except NotImplementedError:
# If the material container is not the most "generic" one it can't be serialized an will raise a
# NotImplementedError. We can simply ignore these.
pass
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
if not self.activePrinter:
# No active printer. Unable to write
return
if self.activePrinter.state not in ["idle", ""]:
# Printer is not able to accept commands.
return
if self._authentication_state != AuthState.Authenticated:
# Not authenticated, so unable to send job.
return
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
# Unable to find g-code. Nothing to send
return
self._gcode = gcode_list
errors = self._checkForErrors()
if errors:
text = i18n_catalog.i18nc("@label", "Unable to start a new print job.")
informative_text = i18n_catalog.i18nc("@label",
"There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
"Please resolve this issues before continuing.")
detailed_text = ""
for error in errors:
detailed_text += error + "\n"
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
text,
informative_text,
detailed_text,
buttons=QMessageBox.Ok,
icon=QMessageBox.Critical,
callback = self._messageBoxCallback
)
return # Don't continue; Errors must block sending the job to the printer.
# There might be multiple things wrong with the configuration. Check these before starting.
warnings = self._checkForWarnings()
if warnings:
text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
informative_text = i18n_catalog.i18nc("@label",
"There is a mismatch between the configuration or calibration of the printer and Cura. "
"For the best result, always slice for the PrintCores and materials that are inserted in your printer.")
detailed_text = ""
for warning in warnings:
detailed_text += warning + "\n"
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
text,
informative_text,
detailed_text,
buttons=QMessageBox.Yes + QMessageBox.No,
icon=QMessageBox.Question,
callback=self._messageBoxCallback
)
return
# No warnings or errors, so we're good to go.
self._startPrint()
# Notify the UI that a switch to the print monitor should happen
Application.getInstance().getController().setActiveStage("MonitorStage")
def _startPrint(self):
Logger.log("i", "Sending print job to printer.")
if self._sending_gcode:
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
self._sending_gcode = True
self._send_gcode_start = time()
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
i18n_catalog.i18nc("@info:title", "Sending Data"))
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
self._progress_message.show()
compressed_gcode = self._compressGCode()
if compressed_gcode is None:
# Abort was called.
return
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
onFinished=self._onPostPrintJobFinished)
return
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
if action_id == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
def _onPostPrintJobFinished(self, reply):
self._progress_message.hide()
self._sending_gcode = False
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
if bytes_total > 0:
new_progress = bytes_sent / bytes_total * 100
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
# timeout responses if this happens.
self._last_response_time = time()
if new_progress > self._progress_message.getProgress():
self._progress_message.show() # Ensure that the message is visible.
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
else:
self._progress_message.setProgress(0)
self._progress_message.hide()
def _messageBoxCallback(self, button):
def delayedCallback():
if button == QMessageBox.Yes:
self._startPrint()
else:
Application.getInstance().getController().setActiveStage("PrepareStage")
# For some unknown reason Cura on OSX will hang if we do the call back code
# immediately without first returning and leaving QML's event system.
QTimer.singleShot(100, delayedCallback)
def _checkForErrors(self):
errors = []
print_information = Application.getInstance().getPrintInformation()
if not print_information.materialLengths:
Logger.log("w", "There is no material length information. Unable to check for errors.")
return errors
for index, extruder in enumerate(self.activePrinter.extruders):
# Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
if extruder.hotendID == "":
# No Printcore loaded.
errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
# The extruder is by this print.
if extruder.activeMaterial is None:
# No active material
errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
return errors
def _checkForWarnings(self):
warnings = []
print_information = Application.getInstance().getPrintInformation()
if not print_information.materialLengths:
Logger.log("w", "There is no material length information. Unable to check for warnings.")
return warnings
extruder_manager = ExtruderManager.getInstance()
for index, extruder in enumerate(self.activePrinter.extruders):
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
# The extruder is by this print.
# TODO: material length check
# Check if the right Printcore is active.
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
if variant:
if variant.getName() != extruder.hotendID:
warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
else:
Logger.log("w", "Unable to find variant.")
# Check if the right material is loaded.
local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
if local_material:
if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
else:
Logger.log("w", "Unable to find material.")
return warnings
def _update(self):
if not super()._update():
return
if self._authentication_state == AuthState.NotAuthenticated:
if self._authentication_id is None and self._authentication_key is None:
# This machine doesn't have any authentication, so request it.
self._requestAuthentication()
elif self._authentication_id is not None and self._authentication_key is not None:
# We have authentication info, but we haven't checked it out yet. Do so now.
self._verifyAuthentication()
elif self._authentication_state == AuthState.AuthenticationReceived:
# We have an authentication, but it's not confirmed yet.
self._checkAuthentication()
# We don't need authentication for requesting info, so we can go right ahead with requesting this.
self.get("printer", onFinished=self._onGetPrinterDataFinished)
self.get("print_job", onFinished=self._onGetPrintJobFinished)
def _resetAuthenticationRequestedMessage(self):
if self._authentication_requested_message:
self._authentication_requested_message.hide()
self._authentication_timer.stop()
self._authentication_counter = 0
def _onAuthenticationTimer(self):
self._authentication_counter += 1
self._authentication_requested_message.setProgress(
self._authentication_counter / self._max_authentication_counter * 100)
if self._authentication_counter > self._max_authentication_counter:
self._authentication_timer.stop()
Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
self.setAuthenticationState(AuthState.AuthenticationDenied)
self._resetAuthenticationRequestedMessage()
self._authentication_failed_message.show()
def _verifyAuthentication(self):
Logger.log("d", "Attempting to verify authentication")
# This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted)
def _onVerifyAuthenticationCompleted(self, reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 401:
# Something went wrong; We somehow tried to verify authentication without having one.
Logger.log("d", "Attempted to verify auth without having one.")
self._authentication_id = None
self._authentication_key = None
self.setAuthenticationState(AuthState.NotAuthenticated)
elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
# If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
Logger.log("d",
"While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
self._authentication_state)
self.setAuthenticationState(AuthState.AuthenticationDenied)
self._authentication_failed_message.show()
elif status_code == 200:
self.setAuthenticationState(AuthState.Authenticated)
# Now we know for sure that we are authenticated, send the material profiles to the machine.
self._sendMaterialProfiles()
def _checkAuthentication(self):
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished)
def _onCheckAuthenticationFinished(self, reply):
if str(self._authentication_id) not in reply.url().toString():
Logger.log("w", "Got an old id response.")
# Got response for old authentication ID.
return
try:
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
return
if data.get("message", "") == "authorized":
Logger.log("i", "Authentication was approved")
self.setAuthenticationState(AuthState.Authenticated)
self._saveAuthentication()
# Double check that everything went well.
self._verifyAuthentication()
# Notify the user.
self._resetAuthenticationRequestedMessage()
self._authentication_succeeded_message.show()
elif data.get("message", "") == "unauthorized":
Logger.log("i", "Authentication was denied.")
self.setAuthenticationState(AuthState.AuthenticationDenied)
self._authentication_failed_message.show()
def _saveAuthentication(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
if "network_authentication_key" in global_container_stack.getMetaData():
global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)
else:
global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key)
if "network_authentication_id" in global_container_stack.getMetaData():
global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)
else:
global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
# Force save so we are sure the data is not lost.
Application.getInstance().saveStack(global_container_stack)
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
self._getSafeAuthKey())
else:
Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
self._getSafeAuthKey())
def _onRequestAuthenticationFinished(self, reply):
try:
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
self.setAuthenticationState(AuthState.NotAuthenticated)
return
self.setAuthenticationState(AuthState.AuthenticationReceived)
self._authentication_id = data["id"]
self._authentication_key = data["key"]
Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
self._authentication_id, self._getSafeAuthKey())
def _requestAuthentication(self):
self._authentication_requested_message.show()
self._authentication_timer.start()
# Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
# give issues.
self._authentication_key = None
self._authentication_id = None
self.post("auth/request",
json.dumps({"application": "Cura-" + Application.getInstance().getVersion(),
"user": self._getUserName()}).encode(),
onFinished=self._onRequestAuthenticationFinished)
self.setAuthenticationState(AuthState.AuthenticationRequested)
def _onAuthenticationRequired(self, reply, authenticator):
if self._authentication_id is not None and self._authentication_key is not None:
Logger.log("d",
"Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
self._id, self._authentication_id, self._getSafeAuthKey())
authenticator.setUser(self._authentication_id)
authenticator.setPassword(self._authentication_key)
else:
Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)
def _onGetPrintJobFinished(self, reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if not self._printers:
return # Ignore the data for now, we don't have info about a printer yet.
printer = self._printers[0]
if status_code == 200:
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
return
if printer.activePrintJob is None:
print_job = PrintJobOutputModel(output_controller=self._output_controller)
printer.updateActivePrintJob(print_job)
else:
print_job = printer.activePrintJob
print_job.updateState(result["state"])
print_job.updateTimeElapsed(result["time_elapsed"])
print_job.updateTimeTotal(result["time_total"])
print_job.updateName(result["name"])
elif status_code == 404:
# No job found, so delete the active print job (if any!)
printer.updateActivePrintJob(None)
else:
Logger.log("w",
"Got status code {status_code} while trying to get printer data".format(status_code=status_code))
def materialHotendChangedMessage(self, callback):
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
i18n_catalog.i18nc("@label",
"Would you like to use your current printer configuration in Cura?"),
i18n_catalog.i18nc("@label",
"The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
buttons=QMessageBox.Yes + QMessageBox.No,
icon=QMessageBox.Question,
callback=callback
)
def _onGetPrinterDataFinished(self, reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 200:
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
return
if not self._printers:
# Quickest way to get the firmware version is to grab it from the zeroconf.
firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
self._printers[0].setCamera(NetworkCamera("http://" + self._address + ":8080/?action=stream"))
for extruder in self._printers[0].extruders:
extruder.activeMaterialChanged.connect(self.materialIdChanged)
extruder.hotendIDChanged.connect(self.hotendIdChanged)
self.printersChanged.emit()
# LegacyUM3 always has a single printer.
printer = self._printers[0]
printer.updateBedTemperature(result["bed"]["temperature"]["current"])
printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
printer.updateState(result["status"])
try:
# If we're still handling the request, we should ignore remote for a bit.
if not printer.getController().isPreheatRequestInProgress():
printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
except KeyError:
# Older firmwares don't support preheating, so we need to fake it.
pass
head_position = result["heads"][0]["position"]
printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])
for index in range(0, self._number_of_extruders):
temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
extruder = printer.extruders[index]
extruder.updateTargetHotendTemperature(temperatures["target"])
extruder.updateHotendTemperature(temperatures["current"])
material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
# Find matching material (as we need to set brand, type & color)
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
GUID=material_guid)
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
# Unknown material.
color = "#00000000"
brand = "Unknown"
material_type = "Unknown"
name = "Unknown"
material = MaterialOutputModel(guid=material_guid, type=material_type,
brand=brand, color=color, name = name)
extruder.updateActiveMaterial(material)
try:
hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
except KeyError:
hotend_id = ""
printer.extruders[index].updateHotendID(hotend_id)
else:
Logger.log("w",
"Got status code {status_code} while trying to get printer data".format(status_code = status_code))
## Convenience function to "blur" out all but the last 5 characters of the auth key.
# This can be used to debug print the key, without it compromising the security.
def _getSafeAuthKey(self):
if self._authentication_key is not None:
result = self._authentication_key[-5:]
result = "********" + result
return result
return self._authentication_key

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
{
Item
{
width: maximumWidth
height: maximumHeight
Image
{
id: cameraImage
property bool proportionalHeight:
{
if(sourceSize.height == 0 || maximumHeight == 0)
{
return true;
}
return (sourceSize.width / sourceSize.height) > (maximumWidth / maximumHeight);
}
property real _width: Math.floor(Math.min(maximumWidth, sourceSize.width))
property real _height: Math.floor(Math.min(maximumHeight, sourceSize.height))
width: proportionalHeight ? _width : Math.floor(sourceSize.width * _height / sourceSize.height)
height: !proportionalHeight ? _height : Math.floor(sourceSize.height * _width / sourceSize.width)
width: Math.min(sourceSize.width === 0 ? 800 * screenScaleFactor : sourceSize.width, maximumWidth)
height: Math.floor((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
z: 1
Component.onCompleted:
{
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.start()
}
}
onVisibleChanged:
{
if(visible)
{
OutputDevice.startCamera()
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.start()
}
} else
{
OutputDevice.stopCamera()
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.stop()
}
}
}
source:
{
if(OutputDevice.cameraImage)
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
{
return OutputDevice.cameraImage;
return OutputDevice.activePrinter.camera.latestImage;
}
return "";
}
}
}
}

View file

@ -1,730 +0,0 @@
import datetime
import getpass
import gzip
import json
import os
import os.path
import time
from enum import Enum
from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
from UM.Application import Application
from UM.Logger import Logger
from UM.Message import Message
from UM.OutputDevice import OutputDeviceError
from UM.i18n import i18nCatalog
from UM.Qt.Duration import Duration, DurationFormat
from UM.PluginRegistry import PluginRegistry
from . import NetworkPrinterOutputDevice
i18n_catalog = i18nCatalog("cura")
class OutputStage(Enum):
ready = 0
uploading = 2
class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinterOutputDevice):
printJobsChanged = pyqtSignal()
printersChanged = pyqtSignal()
selectedPrinterChanged = pyqtSignal()
def __init__(self, key, address, properties, api_prefix):
super().__init__(key, address, properties, api_prefix)
# Store the address of the master.
self._master_address = address
name_property = properties.get(b"name", b"")
if name_property:
name = name_property.decode("utf-8")
else:
name = key
self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated
self.setName(name)
description = i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")
self.setShortDescription(description)
self.setDescription(description)
self._stage = OutputStage.ready
host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "")
if host_override:
Logger.log(
"w",
"Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host",
host_override)
self._host = "http://" + host_override
else:
self._host = "http://" + address
# is the same as in NetworkPrinterOutputDevicePlugin
self._cluster_api_version = "1"
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
self._api_base_uri = self._host + self._cluster_api_prefix
self._file_name = None
self._progress_message = None
self._request = None
self._reply = None
# The main reason to keep the 'multipart' form data on the object
# is to prevent the Python GC from claiming it too early.
self._multipart = None
self._print_view = None
self._request_job = []
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
self._print_jobs = []
self._print_job_by_printer_uuid = {}
self._print_job_by_uuid = {} # Print jobs by their own uuid
self._printers = []
self._printers_dict = {} # by unique_name
self._connected_printers_type_count = []
self._automatic_printer = {"unique_name": "", "friendly_name": "Automatic"} # empty unique_name IS automatic selection
self._selected_printer = self._automatic_printer
self._cluster_status_update_timer = QTimer()
self._cluster_status_update_timer.setInterval(5000)
self._cluster_status_update_timer.setSingleShot(False)
self._cluster_status_update_timer.timeout.connect(self._requestClusterStatus)
self._can_pause = True
self._can_abort = True
self._can_pre_heat_bed = False
self._can_control_manually = False
self._cluster_size = int(properties.get(b"cluster_size", 0))
self._cleanupRequest()
#These are texts that are to be translated for future features.
temporary_translation = i18n_catalog.i18n("This printer is not set up to host a group of connected Ultimaker 3 printers.")
temporary_translation2 = i18n_catalog.i18nc("Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers.").format(count = 3)
temporary_translation3 = i18n_catalog.i18n("{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate.") #When finished.
temporary_translation4 = i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") #When configuration changed.
## No authentication, so requestAuthentication should do exactly nothing
@pyqtSlot()
def requestAuthentication(self, message_id = None, action_id = "Retry"):
pass # Cura Connect doesn't do any authorization
def setAuthenticationState(self, auth_state):
self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated
def _verifyAuthentication(self):
pass
def _checkAuthentication(self):
Logger.log("d", "_checkAuthentication Cura Connect - nothing to be done")
@pyqtProperty(QObject, notify=selectedPrinterChanged)
def controlItem(self):
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if not self._control_item:
self._createControlViewFromQML()
name = self._selected_printer.get("friendly_name")
if name == self._automatic_printer.get("friendly_name") or name == "":
return self._control_item
# Let cura use the default.
return None
@pyqtSlot(int, result = str)
def getTimeCompleted(self, time_remaining):
current_time = time.time()
datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute)
@pyqtSlot(int, result = str)
def getDateCompleted(self, time_remaining):
current_time = time.time()
datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
@pyqtProperty(int, constant = True)
def clusterSize(self):
return self._cluster_size
@pyqtProperty(str, notify=selectedPrinterChanged)
def name(self):
# Show the name of the selected printer.
# This is not the nicest way to do this, but changes to the Cura UI are required otherwise.
name = self._selected_printer.get("friendly_name")
if name != self._automatic_printer.get("friendly_name"):
return name
# Return name of cluster master.
return self._properties.get(b"name", b"").decode("utf-8")
def connect(self):
super().connect()
self._cluster_status_update_timer.start()
def close(self):
super().close()
self._cluster_status_update_timer.stop()
def _setJobState(self, job_state):
if not self._selected_printer:
return
selected_printer_uuid = self._printers_dict[self._selected_printer["unique_name"]]["uuid"]
if selected_printer_uuid not in self._print_job_by_printer_uuid:
return
print_job_uuid = self._print_job_by_printer_uuid[selected_printer_uuid]["uuid"]
url = QUrl(self._api_base_uri + "print_jobs/" + print_job_uuid + "/action")
put_request = QNetworkRequest(url)
put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
data = '{"action": "' + job_state + '"}'
self._manager.put(put_request, data.encode())
def _requestClusterStatus(self):
# TODO: Handle timeout. We probably want to know if the cluster is still reachable or not.
url = QUrl(self._api_base_uri + "printers/")
printers_request = QNetworkRequest(url)
self._addUserAgentHeader(printers_request)
self._manager.get(printers_request)
# See _finishedPrintersRequest()
if self._printers: # if printers is not empty
url = QUrl(self._api_base_uri + "print_jobs/")
print_jobs_request = QNetworkRequest(url)
self._addUserAgentHeader(print_jobs_request)
self._manager.get(print_jobs_request)
# See _finishedPrintJobsRequest()
def _finishedPrintJobsRequest(self, reply):
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
return
self.setPrintJobs(json_data)
def _finishedPrintersRequest(self, reply):
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
return
self.setPrinters(json_data)
def materialHotendChangedMessage(self, callback):
# When there is just one printer, the activate configuration option is enabled
if (self._cluster_size == 1):
super().materialHotendChangedMessage(callback = callback)
def _startCameraStream(self):
## Request new image
url = QUrl("http://" + self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream")
self._image_request = QNetworkRequest(url)
self._addUserAgentHeader(self._image_request)
self._image_reply = self._manager.get(self._image_request)
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
def spawnPrintView(self):
if self._print_view is None:
path = os.path.join(self._plugin_path, "PrintWindow.qml")
self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
if self._print_view is not None:
self._print_view.show()
## Store job info, show Print view for settings
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
self._selected_printer = self._automatic_printer # reset to default option
self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs]
# the build plates to be sent
gcodes = getattr(Application.getInstance().getController().getScene(), "gcode_list")
self._job_list = list(gcodes.keys())
Logger.log("d", "build plates to be sent to printer: %s", (self._job_list))
if self._stage != OutputStage.ready:
if self._error_message:
self._error_message.hide()
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
self._add_build_plate_number = len(self._job_list) > 1
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
if len(self._printers) > 1:
self.spawnPrintView() # Ask user how to print it.
elif len(self._printers) == 1:
# If there is only one printer, don't bother asking.
self.selectAutomaticPrinter()
self.sendPrintJob()
else:
# Cluster has no printers, warn the user of this.
if self._error_message:
self._error_message.hide()
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers."))
self._error_message.show()
## Actually send the print job, called from the dialog
# :param: require_printer_name: name of printer, or ""
@pyqtSlot()
def sendPrintJob(self):
nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job
output_build_plate_number = self._job_list.pop(0)
gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[output_build_plate_number]
self._send_gcode_start = time.time()
Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number))
if self._stage != OutputStage.ready:
Logger.log("d", "Unable to send print job as the state is %s", self._stage)
raise OutputDeviceError.DeviceBusyError()
self._stage = OutputStage.uploading
if self._add_build_plate_number:
self._file_name = "%s_%d.gcode.gz" % (file_name, output_build_plate_number)
else:
self._file_name = "%s.gcode.gz" % (file_name)
self._showProgressMessage()
require_printer_name = self._selected_printer["unique_name"]
new_request = self._buildSendPrintJobHttpRequest(require_printer_name, gcode)
if new_request is None or self._stage != OutputStage.uploading:
return
self._request = new_request
self._reply = self._manager.post(self._request, self._multipart)
self._reply.uploadProgress.connect(self._onUploadProgress)
# See _finishedPrintJobPostRequest()
def _buildSendPrintJobHttpRequest(self, require_printer_name, gcode):
api_url = QUrl(self._api_base_uri + "print_jobs/")
request = QNetworkRequest(api_url)
# Create multipart request and add the g-code.
self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType)
# Add gcode
part = QHttpPart()
part.setHeader(QNetworkRequest.ContentDispositionHeader,
'form-data; name="file"; filename="%s"' % (self._file_name))
compressed_gcode = self._compressGcode(gcode)
if compressed_gcode is None:
return None # User aborted print, so stop trying.
part.setBody(compressed_gcode)
self._multipart.append(part)
# require_printer_name "" means automatic
if require_printer_name:
self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name))
user_name = self.__get_username()
if user_name is None:
user_name = "unknown"
self._multipart.append(self.__createKeyValueHttpPart("owner", user_name))
self._addUserAgentHeader(request)
return request
def _compressGcode(self, gcode):
self._compressing_print = True
batched_line = ""
max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB
byte_array_file_data = b""
def _compressDataAndNotifyQt(data_to_append):
compressed_data = gzip.compress(data_to_append.encode("utf-8"))
self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Pretend that this is a response, as zipping might take a bit of time.
self._last_response_time = time.time()
return compressed_data
if gcode is None:
Logger.log("e", "Unable to find sliced gcode, returning empty.")
return byte_array_file_data
for line in gcode:
if not self._compressing_print:
self._progress_message.hide()
return None # Stop trying to zip, abort was called.
batched_line += line
# if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
# Compressing line by line in this case is extremely slow, so we need to batch them.
if len(batched_line) < max_chars_per_line:
continue
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
batched_line = ""
# Also compress the leftovers.
if batched_line:
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
return byte_array_file_data
def __createKeyValueHttpPart(self, key, value):
metadata_part = QHttpPart()
metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain')
metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key))
metadata_part.setBody(bytearray(value, "utf8"))
return metadata_part
def __get_username(self):
try:
return getpass.getuser()
except:
Logger.log("d", "Could not get the system user name, returning 'unknown' instead.")
return None
def _finishedPrintJobPostRequest(self, reply):
self._stage = OutputStage.ready
if self._progress_message:
self._progress_message.hide()
self._progress_message = None
self.writeFinished.emit(self)
if reply.error():
self._showRequestFailedMessage(reply)
self.writeError.emit(self)
else:
self._showRequestSucceededMessage()
self.writeSuccess.emit(self)
self._cleanupRequest()
if self._job_list: # start sending next job
self.sendPrintJob()
def _showRequestFailedMessage(self, reply):
if reply is not None:
Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format(
cluster_name = self.getName(),
error_string = str(reply.errorString()),
error = str(reply.error())))
error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.")
message = Message(text=error_message_template.format(
cluster_name = self.getName()))
message.show()
def _showRequestSucceededMessage(self):
confirmation_message_template = i18n_catalog.i18nc(
"@info:status",
"Sent {file_name} to group {cluster_name}."
)
file_name = os.path.basename(self._file_name).split(".")[0]
message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
message = Message(text=message_text)
button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
message.addAction("open_browser", button_text, "globe", button_tooltip)
message.actionTriggered.connect(self._onMessageActionTriggered)
message.show()
def setPrintJobs(self, print_jobs):
#TODO: hack, last seen messes up the check, so drop it.
for job in print_jobs:
del job["last_seen"]
# Strip any extensions
job["name"] = self._removeGcodeExtension(job["name"])
if self._print_jobs != print_jobs:
old_print_jobs = self._print_jobs
self._print_jobs = print_jobs
self._notifyFinishedPrintJobs(old_print_jobs, print_jobs)
self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs)
# Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer
# for some reason. ugh.
self._print_job_by_printer_uuid = {}
self._print_job_by_uuid = {}
for print_job in print_jobs:
if "printer_uuid" in print_job and print_job["printer_uuid"] is not None:
self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job
self._print_job_by_uuid[print_job["uuid"]] = print_job
self.printJobsChanged.emit()
def _removeGcodeExtension(self, name):
parts = name.split(".")
if parts[-1].upper() == "GZ":
parts = parts[:-1]
if parts[-1].upper() == "GCODE":
parts = parts[:-1]
return ".".join(parts)
def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs):
"""Notify the user when any of their print jobs have just completed.
Arguments:
old_print_jobs -- the previous list of print job status information as returned by the cluster REST API.
new_print_jobs -- the current list of print job status information as returned by the cluster REST API.
"""
if old_print_jobs is None:
return
username = self.__get_username()
if username is None:
return
our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs)
our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"]
our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs)
our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"]
old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs])
for print_job in our_new_finished_print_jobs:
if print_job["uuid"] in old_not_finished_print_job_uuids:
printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
if printer_name is None:
printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")
message_text = (i18n_catalog.i18nc("@info:status",
"Printer '{printer_name}' has finished printing '{job_name}'.")
.format(printer_name=printer_name, job_name=print_job["name"]))
message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished"))
Application.getInstance().showMessage(message)
Application.getInstance().showToastMessage(
i18n_catalog.i18nc("@info:status", "Print finished"),
message_text)
def __filterOurPrintJobs(self, print_jobs):
username = self.__get_username()
return [print_job for print_job in print_jobs if print_job["owner"] == username]
def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs):
if old_print_jobs is None:
return
old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs))
new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs))
old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs])
for print_job in new_change_required_print_jobs:
if print_job["uuid"] not in old_change_required_print_job_uuids:
printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"])
if printer_name is None:
# don't report on yet unknown printers
continue
message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.")
.format(printer_name=printer_name, job_name=print_job["name"]))
message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required"))
Application.getInstance().showMessage(message)
Application.getInstance().showToastMessage(
i18n_catalog.i18nc("@label:status", "Action required"),
message_text)
def __filterConfigChangePrintJobs(self, print_jobs):
return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs)
def __isConfigurationChangeRequiredPrintJob(self, print_job):
if print_job["status"] == "queued":
changes_required = print_job.get("configuration_changes_required", [])
return len(changes_required) != 0
return False
def __getPrinterNameFromUuid(self, printer_uuid):
for printer in self._printers:
if printer["uuid"] == printer_uuid:
return printer["friendly_name"]
return None
def setPrinters(self, printers):
if self._printers != printers:
self._connected_printers_type_count = []
printers_count = {}
self._printers = printers
self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name
for printer in printers:
variant = printer["machine_variant"]
if variant in printers_count:
printers_count[variant] += 1
else:
printers_count[variant] = 1
for type in printers_count:
self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]})
self.printersChanged.emit()
@pyqtProperty("QVariantList", notify=printersChanged)
def connectedPrintersTypeCount(self):
return self._connected_printers_type_count
@pyqtProperty("QVariantList", notify=printersChanged)
def connectedPrinters(self):
return self._printers
@pyqtProperty(int, notify=printJobsChanged)
def numJobsPrinting(self):
num_jobs_printing = 0
for job in self._print_jobs:
if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]:
num_jobs_printing += 1
return num_jobs_printing
@pyqtProperty(int, notify=printJobsChanged)
def numJobsQueued(self):
num_jobs_queued = 0
for job in self._print_jobs:
if job["status"] == "queued":
num_jobs_queued += 1
return num_jobs_queued
@pyqtProperty("QVariantMap", notify=printJobsChanged)
def printJobsByUUID(self):
return self._print_job_by_uuid
@pyqtProperty("QVariantMap", notify=printJobsChanged)
def printJobsByPrinterUUID(self):
return self._print_job_by_printer_uuid
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self):
return self._print_jobs
@pyqtProperty("QVariantList", notify=printersChanged)
def printers(self):
return [self._automatic_printer, ] + self._printers
@pyqtSlot(str, str)
def selectPrinter(self, unique_name, friendly_name):
self.stopCamera()
self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name}
Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name)
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if unique_name == "":
self._address = self._master_address
else:
self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"]
self.selectedPrinterChanged.emit()
def _updateJobState(self, job_state):
name = self._selected_printer.get("friendly_name")
if name == "" or name == "Automatic":
# TODO: This is now a bit hacked; If no printer is selected, don't show job state.
if self._job_state != "":
self._job_state = ""
self.jobStateChanged.emit()
else:
if self._job_state != job_state:
self._job_state = job_state
self.jobStateChanged.emit()
@pyqtSlot()
def selectAutomaticPrinter(self):
self.stopCamera()
self._selected_printer = self._automatic_printer
self.selectedPrinterChanged.emit()
@pyqtProperty("QVariant", notify=selectedPrinterChanged)
def selectedPrinterName(self):
return self._selected_printer.get("unique_name", "")
def getPrintJobsUrl(self):
return self._host + "/print_jobs"
def getPrintersUrl(self):
return self._host + "/printers"
def _showProgressMessage(self):
progress_message_template = i18n_catalog.i18nc("@info:progress",
"Sending <filename>{file_name}</filename> to group {cluster_name}")
file_name = os.path.basename(self._file_name).split(".")[0]
self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1)
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
self._progress_message.actionTriggered.connect(self._onMessageActionTriggered)
self._progress_message.show()
def _addUserAgentHeader(self, request):
request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin")
def _cleanupRequest(self):
self._request = None
self._stage = OutputStage.ready
self._file_name = None
def _onFinished(self, reply):
super()._onFinished(reply)
reply_url = reply.url().toString()
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 500:
Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url))
return
if reply.error() == QNetworkReply.ContentOperationNotPermittedError:
# It was probably "/api/v1/materials" for legacy UM3
return
if reply.error() == QNetworkReply.ContentNotFoundError:
# It was probably "/api/v1/print_job" for legacy UM3
return
if reply.operation() == QNetworkAccessManager.PostOperation:
if self._cluster_api_prefix + "print_jobs" in reply_url:
self._finishedPrintJobPostRequest(reply)
return
# We need to do this check *after* we process the post operation!
# If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this.
if reply.error() != QNetworkReply.NoError:
Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error())
return
elif reply.operation() == QNetworkAccessManager.GetOperation:
if self._cluster_api_prefix + "print_jobs" in reply_url:
self._finishedPrintJobsRequest(reply)
elif self._cluster_api_prefix + "printers" in reply_url:
self._finishedPrintersRequest(reply)
@pyqtSlot()
def openPrintJobControlPanel(self):
Logger.log("d", "Opening print job control panel...")
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
@pyqtSlot()
def openPrinterControlPanel(self):
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl(self.getPrintersUrl()))
def _onMessageActionTriggered(self, message, action):
if action == "open_browser":
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
if action == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
self._compressing_print = False
if self._reply:
self._reply.abort()
self._stage = OutputStage.ready
Application.getInstance().getController().setActiveStage("PrepareStage")
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
## For cluster below
def _get_plugin_directory_name(self):
current_file_absolute_path = os.path.realpath(__file__)
directory_path = os.path.dirname(current_file_absolute_path)
_, directory_name = os.path.split(directory_path)
return directory_name
@property
def _plugin_path(self):
return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name())

File diff suppressed because it is too large Load diff

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ Item
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested
property bool authenticationRequested: printerConnected && (Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 || Cura.MachineManager.printerOutputDevices[0].authenticationState == 5) // AuthState.AuthenticationRequested or AuthenticationReceived.
Row
{
@ -115,22 +115,8 @@ Item
{
tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura")
text: catalog.i18nc("@action:button", "Activate Configuration")
visible: printerConnected && !isClusterPrinter()
visible: false // printerConnected && !isClusterPrinter()
onClicked: manager.loadConfigurationFromPrinter()
function isClusterPrinter() {
if(Cura.MachineManager.printerOutputDevices.length == 0)
{
return false;
}
var clusterSize = Cura.MachineManager.printerOutputDevices[0].clusterSize;
// This is not a cluster printer or the cluster it is just one printer
if(clusterSize == undefined || clusterSize == 1)
{
return false;
}
return true;
}
}
}

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.
from . import NetworkPrinterOutputDevicePlugin
from . import DiscoverUM3Action
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from . import UM3OutputDevicePlugin
def getMetaData():
return {}
def register(app):
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}

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

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.
from UM.Signal import Signal, signalemitter
from . import USBPrinterOutputDevice
from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from cura.PrinterOutputDevice import ConnectionState
from UM.Qt.ListModel import ListModel
from UM.Message import Message
from UM.i18n import i18nCatalog
from cura.PrinterOutputDevice import ConnectionState
from cura.CuraApplication import CuraApplication
from . import USBPrinterOutputDevice
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
import threading
import platform
import time
import os.path
import serial.tools.list_ports
from UM.Extension import Extension
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer.
## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.
@signalemitter
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
addUSBOutputDeviceSignal = Signal()
progressChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent = parent)
self._serial_port_list = []
@ -38,39 +37,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
self._update_thread.setDaemon(True)
self._check_updates = True
self._firmware_view = None
Application.getInstance().applicationShuttingDown.connect(self.stop)
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
addUSBOutputDeviceSignal = Signal()
connectionStateChanged = pyqtSignal()
progressChanged = pyqtSignal()
firmwareUpdateChange = pyqtSignal()
@pyqtProperty(float, notify = progressChanged)
def progress(self):
progress = 0
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
progress += device.progress
return progress / len(self._usb_output_devices)
@pyqtProperty(int, notify = progressChanged)
def errorCode(self):
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
if device._error_code:
return device._error_code
return 0
## Return True if all printers finished firmware update
@pyqtProperty(float, notify = firmwareUpdateChange)
def firmwareUpdateCompleteStatus(self):
complete = True
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
if not device.firmwareUpdateFinished:
complete = False
return complete
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice)
def start(self):
self._check_updates = True
@ -79,58 +49,28 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
def stop(self):
self._check_updates = False
def _updateThread(self):
while self._check_updates:
result = self.getSerialPortList(only_list_usb = True)
self._addRemovePorts(result)
time.sleep(5)
## Show firmware interface.
# This will create the view if its not already created.
def spawnFirmwareInterface(self, serial_port):
if self._firmware_view is None:
path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._firmware_view.show()
@pyqtSlot(str)
def updateAllFirmware(self, file_name):
if file_name.startswith("file://"):
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
if not self._usb_output_devices:
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
def _onConnectionStateChanged(self, serial_port):
if serial_port not in self._usb_output_devices:
return
for printer_connection in self._usb_output_devices:
self._usb_output_devices[printer_connection].resetFirmwareUpdate()
self.spawnFirmwareInterface("")
for printer_connection in self._usb_output_devices:
try:
self._usb_output_devices[printer_connection].updateFirmware(file_name)
except FileNotFoundError:
# Should only happen in dev environments where the resources/firmware folder is absent.
self._usb_output_devices[printer_connection].setProgress(100, 100)
Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name)
Message(i18n_catalog.i18nc("@info",
"Could not find firmware required for the printer at %s.") % printer_connection, title = i18n_catalog.i18nc("@info:title", "Printer Firmware")).show()
self._firmware_view.close()
changed_device = self._usb_output_devices[serial_port]
if changed_device.connectionState == ConnectionState.connected:
self.getOutputDeviceManager().addOutputDevice(changed_device)
else:
self.getOutputDeviceManager().removeOutputDevice(serial_port)
def _updateThread(self):
while self._check_updates:
container_stack = Application.getInstance().getGlobalContainerStack()
if container_stack is None:
time.sleep(5)
continue
@pyqtSlot(str, str, result = bool)
def updateFirmwareBySerial(self, serial_port, file_name):
if serial_port in self._usb_output_devices:
self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort())
try:
self._usb_output_devices[serial_port].updateFirmware(file_name)
except FileNotFoundError:
self._firmware_view.close()
Logger.log("e", "Could not find firmware required for this machine called '%s'", file_name)
return False
return True
return False
if container_stack.getMetaDataEntry("supports_usb_connection"):
port_list = self.getSerialPortList(only_list_usb=True)
else:
port_list = [] # Just use an empty list; all USB devices will be removed.
self._addRemovePorts(port_list)
time.sleep(5)
## Return the singleton instance of the USBPrinterManager
@classmethod
@ -191,7 +131,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
Logger.log("w", "There is no firmware for machine %s.", machine_id)
if hex_file:
try:
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
except FileNotFoundError:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
else:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
@ -205,46 +149,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
continue
self._serial_port_list = list(serial_ports)
devices_to_remove = []
for port, device in self._usb_output_devices.items():
if port not in self._serial_port_list:
device.close()
devices_to_remove.append(port)
for port in devices_to_remove:
del self._usb_output_devices[port]
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addOutputDevice(self, serial_port):
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
device.connectionStateChanged.connect(self._onConnectionStateChanged)
device.connect()
device.progressChanged.connect(self.progressChanged)
device.firmwareUpdateChange.connect(self.firmwareUpdateChange)
self._usb_output_devices[serial_port] = device
## If one of the states of the connected devices change, we might need to add / remove them from the global list.
def _onConnectionStateChanged(self, serial_port):
success = True
try:
if self._usb_output_devices[serial_port].connectionState == ConnectionState.connected:
self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port])
else:
success = success and self.getOutputDeviceManager().removeOutputDevice(serial_port)
if success:
self.connectionStateChanged.emit()
except KeyError:
Logger.log("w", "Connection state of %s changed, but it was not found in the list")
@pyqtProperty(QObject , notify = connectionStateChanged)
def connectedPrinterList(self):
self._usb_output_devices_model = ListModel()
self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name")
self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer")
for connection in self._usb_output_devices:
if self._usb_output_devices[connection].connectionState == ConnectionState.connected:
self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]})
return self._usb_output_devices_model
device.connect()
## Create a list of serial ports on the system.
# \param only_list_usb If true, only usb ports are listed

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.
from . import USBPrinterOutputDeviceManager
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
from PyQt5.QtQml import qmlRegisterSingletonType
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
}
return {}
def register(app):
# We are violating the QT API here (as we use a factory, which is technically not allowed).
# but we don't really have another means for doing this (and it seems to you know -work-)
qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
return {"extension":USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance(), "output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}
return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}

View file

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

View file

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

View file

@ -3633,7 +3633,7 @@
"minimum_value": "0",
"maximum_value_warning": "100",
"default_value": 15,
"value": "15 if support_enable else 0",
"value": "15 if support_enable else 0 if support_tree_enable else 15",
"enabled": "support_enable or support_tree_enable",
"limit_to_extruder": "support_infill_extruder_nr",
"settable_per_mesh": false,
@ -4233,6 +4233,18 @@
"limit_to_extruder": "support_infill_extruder_nr",
"enabled": "support_enable and support_use_towers",
"settable_per_mesh": true
},
"support_mesh_drop_down":
{
"label": "Drop Down Support Mesh",
"description": "Make support everywhere below the support mesh, so that there's no overhang in the support mesh.",
"type": "bool",
"default_value": true,
"enabled": "support_mesh",
"settable_per_mesh": true,
"settable_per_extruder": false,
"settable_per_meshgroup": false,
"settable_globally": false
}
}
},
@ -5261,18 +5273,6 @@
"settable_per_meshgroup": false,
"settable_globally": false
},
"support_mesh_drop_down":
{
"label": "Drop Down Support Mesh",
"description": "Make support everywhere below the support mesh, so that there's no overhang in the support mesh.",
"type": "bool",
"default_value": true,
"enabled": "support_mesh",
"settable_per_mesh": true,
"settable_per_extruder": false,
"settable_per_meshgroup": false,
"settable_globally": false
},
"anti_overhang_mesh":
{
"label": "Anti Overhang Mesh",

View file

@ -1,4 +1,5 @@
{
"id": "malyan_m180",
"version": 2,
"name": "Malyan M180",
"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" }
}
}

Binary file not shown.

View file

@ -395,6 +395,29 @@ UM.MainWindow
anchors.top: parent.top
}
Loader
{
id: main
anchors
{
top: topbar.bottom
bottom: parent.bottom
left: parent.left
right: sidebar.left
}
MouseArea
{
visible: UM.Controller.activeStage.mainComponent != ""
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel: wheel.accepted = true
}
source: UM.Controller.activeStage.mainComponent
}
Loader
{
id: sidebar
@ -455,29 +478,6 @@ UM.MainWindow
}
}
Loader
{
id: main
anchors
{
top: topbar.bottom
bottom: parent.bottom
left: parent.left
right: sidebar.left
}
MouseArea
{
visible: UM.Controller.activeStage.mainComponent != ""
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel: wheel.accepted = true
}
source: UM.Controller.activeStage.mainComponent
}
UM.MessageStack
{
anchors

View file

@ -47,6 +47,7 @@ Menu
{
model: Cura.BuildPlateModel
MenuItem {
enabled: UM.Selection.hasSelection
text: Cura.BuildPlateModel.getItem(index).name;
onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.getItem(index).buildPlateNumber);
checkable: true
@ -58,6 +59,7 @@ Menu
}
MenuItem {
enabled: UM.Selection.hasSelection
text: "New build plate";
onTriggered: {
CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.maxBuildPlate + 1);

View file

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

View file

@ -105,7 +105,6 @@ Rectangle
topMargin: UM.Theme.getSize("default_margin").height;
left: parent.left;
leftMargin: UM.Theme.getSize("default_margin").height;
//bottom: objectsList.top;
bottomMargin: UM.Theme.getSize("default_margin").height;
}
@ -139,7 +138,7 @@ Rectangle
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30
text: Cura.ObjectsModel.getItem(index) ? Cura.ObjectsModel.getItem(index).name : "";
text: (index >= 0) && Cura.ObjectsModel.getItem(index) ? Cura.ObjectsModel.getItem(index).name : "";
color: Cura.ObjectsModel.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectsModel.getItem(index).isOutsideBuildArea ? palette.mid : palette.text)
elide: Text.ElideRight
}

View file

@ -453,34 +453,6 @@ UM.PreferencesPage
text: catalog.i18nc("@label","Opening and saving files")
}
UM.TooltipArea {
width: childrenRect.width
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","Use multi build plate functionality (EXPERIMENTAL)")
CheckBox
{
id: useMultiBuildPlateCheckbox
text: catalog.i18nc("@option:check","Use multi build plate functionality (EXPERIMENTAL, restart)")
checked: boolCheck(UM.Preferences.getValue("cura/use_multi_build_plate"))
onCheckedChanged: UM.Preferences.setValue("cura/use_multi_build_plate", checked)
}
}
UM.TooltipArea {
width: childrenRect.width
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)")
CheckBox
{
id: arrangeOnLoadCheckbox
text: catalog.i18nc("@option:check","Arrange objects on load (EXPERIMENTAL)")
checked: boolCheck(UM.Preferences.getValue("cura/arrange_objects_on_load"))
onCheckedChanged: UM.Preferences.setValue("cura/arrange_objects_on_load", checked)
}
}
UM.TooltipArea {
width: childrenRect.width
height: childrenRect.height
@ -688,6 +660,49 @@ UM.PreferencesPage
onCheckedChanged: UM.Preferences.setValue("info/send_slice_info", checked)
}
}
Item
{
//: Spacer
height: UM.Theme.getSize("default_margin").height
width: UM.Theme.getSize("default_margin").height
}
Label
{
font.bold: true
text: catalog.i18nc("@label","Experimental")
}
UM.TooltipArea {
width: childrenRect.width
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","Use multi build plate functionality")
CheckBox
{
id: useMultiBuildPlateCheckbox
text: catalog.i18nc("@option:check","Use multi build plate functionality (restart required)")
checked: boolCheck(UM.Preferences.getValue("cura/use_multi_build_plate"))
onCheckedChanged: UM.Preferences.setValue("cura/use_multi_build_plate", checked)
}
}
UM.TooltipArea {
width: childrenRect.width
height: childrenRect.height
text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)")
CheckBox
{
id: arrangeOnLoadCheckbox
text: catalog.i18nc("@option:check","Do not arrange objects on load")
checked: boolCheck(UM.Preferences.getValue("cura/not_arrange_objects_on_load"))
onCheckedChanged: UM.Preferences.setValue("cura/not_arrange_objects_on_load", checked)
}
}
}
}
}

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

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