Merge branch 'feature_intent' of github.com:Ultimaker/Cura into feature_intent_container_tree

This commit is contained in:
Jaime van Kessel 2019-08-27 14:18:41 +02:00
commit d1a8ce54a1
52 changed files with 774 additions and 290 deletions

View file

@ -59,6 +59,9 @@ class MachineInfo:
self.container_id = None
self.name = None
self.definition_id = None
self.metadata_dict = {} # type: Dict[str, str]
self.quality_type = None
self.custom_quality_name = None
self.quality_changes_info = None
@ -342,6 +345,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack_id = self._stripFileToId(global_stack_file)
serialized = archive.open(global_stack_file).read().decode("utf-8")
machine_name = self._getMachineNameFromSerializedStack(serialized)
self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized)
stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine")
self._is_same_machine_type = True
existing_global_stack = None
@ -832,7 +837,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.quality_changes_info.name = quality_changes_name
def _clearStack(self, stack):
@staticmethod
def _clearStack(stack):
application = CuraApplication.getInstance()
stack.definitionChanges.clear()
@ -978,6 +984,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack.setMetaDataEntry("enabled", "True")
extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled))
# Set metadata fields that are missing from the global stack
for key, value in self._machine_info.metadata_dict.items():
if key not in global_stack.getMetaData():
global_stack.setMetaDataEntry(key, value)
def _updateActiveMachine(self, global_stack):
# Actually change the active machine.
machine_manager = Application.getInstance().getMachineManager()
@ -986,6 +997,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
machine_manager.setActiveMachine(global_stack.getId())
# Set metadata fields that are missing from the global stack
for key, value in self._machine_info.metadata_dict.items():
if key not in global_stack.getMetaData():
global_stack.setMetaDataEntry(key, value)
if self._quality_changes_to_apply:
quality_changes_group_dict = quality_manager.getQualityChangesGroups(global_stack)
if self._quality_changes_to_apply not in quality_changes_group_dict:
@ -1012,7 +1028,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop())
def _stripFileToId(self, file):
@staticmethod
def _stripFileToId(file):
mime_type = MimeTypeDatabase.getMimeTypeForFile(file)
file = mime_type.stripExtension(file)
return file.replace("Cura/", "")
@ -1021,7 +1038,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
def _getContainerIdListFromSerialized(self, serialized):
@staticmethod
def _getContainerIdListFromSerialized(serialized):
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized)
@ -1042,12 +1060,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return container_ids
def _getMachineNameFromSerializedStack(self, serialized):
@staticmethod
def _getMachineNameFromSerializedStack(serialized):
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized)
return parser["general"].get("name", "")
def _getMaterialLabelFromSerialized(self, serialized):
@staticmethod
def _getMetaDataDictFromSerializedStack(serialized: str) -> Dict[str, str]:
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized)
return dict(parser["metadata"])
@staticmethod
def _getMaterialLabelFromSerialized(serialized):
data = ET.fromstring(serialized)
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
for entry in metadata:

View file

@ -671,14 +671,20 @@ class CuraEngineBackend(QObject, Backend):
#
# \param message The protobuf message containing g-code, encoded as UTF-8.
def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None:
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
try:
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
pass # Throw the message away.
## 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: Arcus.PythonMessage) -> None:
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
try:
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
pass # Throw the message away.
## Creates a new socket connection.
def _createSocket(self, protocol_file: str = None) -> None:

View file

@ -10,6 +10,9 @@ from UM.Version import Version
import urllib.request
from urllib.error import URLError
from typing import Dict, Optional
import ssl
import certifi
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
@ -39,8 +42,12 @@ class FirmwareUpdateCheckerJob(Job):
result = self.STRING_ZERO_VERSION
try:
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2)
context.load_verify_locations(cafile = certifi.where())
request = urllib.request.Request(url, headers = self._headers)
response = urllib.request.urlopen(request)
response = urllib.request.urlopen(request, context = context)
result = response.read().decode("utf-8")
except URLError:
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
@ -104,7 +111,7 @@ class FirmwareUpdateCheckerJob(Job):
# because the new version of Cura will be release before the firmware and we don't want to
# notify the user when no new firmware version is available.
if (checked_version != "") and (checked_version != current_version):
Logger.log("i", "Showing firmware update message for new version: {version}".format(current_version))
Logger.log("i", "Showing firmware update message for new version: {version}".format(version = current_version))
message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name,
self._lookups.getRedirectUserUrl())
message.actionTriggered.connect(self._callback)
@ -113,7 +120,7 @@ class FirmwareUpdateCheckerJob(Job):
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(self._machine_name))
except Exception as e:
Logger.log("w", "Failed to check for new version: %s", e)
Logger.logException("w", "Failed to check for new version: %s", e)
if not self.silent:
Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show()
return

View file

@ -142,6 +142,18 @@ Item
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.SimpleCheckBox // "Heated build volume"
{
id: heatedVolumeCheckBox
containerStackId: machineStackId
settingKey: "machine_heated_build_volume"
settingStoreIndex: propertyStoreIndex
labelText: catalog.i18nc("@label", "Heated build volume")
labelFont: base.labelFont
labelWidth: base.labelWidth
forceUpdateOnChangeFunction: forceUpdateFunction
}
Cura.ComboBoxWithOptions // "G-code flavor"
{
id: gcodeFlavorComboBox

View file

@ -83,9 +83,13 @@ class SimulationView(CuraView):
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
self._old_composite_shader = None # type: Optional["ShaderProgram"]
self._max_feedrate = sys.float_info.min
self._min_feedrate = sys.float_info.max
self._max_thickness = sys.float_info.min
self._min_thickness = sys.float_info.max
self._global_container_stack = None # type: Optional[ContainerStack]
self._proxy = SimulationViewProxy()
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._proxy = None
self._resetSettings()
self._legend_items = None
@ -104,7 +108,6 @@ class SimulationView(CuraView):
Application.getInstance().getPreferences().addPreference("layerview/show_skin", True)
Application.getInstance().getPreferences().addPreference("layerview/show_infill", True)
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._updateWithPreferences()
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
@ -180,8 +183,7 @@ class SimulationView(CuraView):
def _onSceneChanged(self, node: "SceneNode") -> None:
if node.getMeshData() is None:
self.resetLayerData()
return
self.setActivity(False)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
@ -441,6 +443,8 @@ class SimulationView(CuraView):
## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
# as this caused some issues.
def getProxy(self, engine, script_engine):
if self._proxy is None:
self._proxy = SimulationViewProxy(self)
return self._proxy
def endRendering(self) -> None:
@ -460,6 +464,10 @@ class SimulationView(CuraView):
return True
if event.type == Event.ViewActivateEvent:
# Start listening to changes.
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
# FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching.
# This can happen when you do the following steps:
# 1. Start Cura
@ -506,6 +514,8 @@ class SimulationView(CuraView):
self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
elif event.type == Event.ViewDeactivateEvent:
self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged)
Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged)
self._wireprint_warning_message.hide()
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
if self._global_container_stack:

View file

@ -1,21 +1,24 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
import SimulationView
if TYPE_CHECKING:
from .SimulationView import SimulationView
class SimulationViewProxy(QObject):
def __init__(self, parent=None):
def __init__(self, simulation_view: "SimulationView", parent=None):
super().__init__(parent)
self._simulation_view = simulation_view
self._current_layer = 0
self._controller = Application.getInstance().getController()
self._controller.activeViewChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
self.is_simulationView_selected = False
self._onActiveViewChanged()
currentLayerChanged = pyqtSignal()
currentPathChanged = pyqtSignal()
@ -28,182 +31,112 @@ class SimulationViewProxy(QObject):
@pyqtProperty(bool, notify=activityChanged)
def layerActivity(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getActivity()
return False
return self._simulation_view.getActivity()
@pyqtProperty(int, notify=maxLayersChanged)
def numLayers(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxLayers()
return 0
return self._simulation_view.getMaxLayers()
@pyqtProperty(int, notify=currentLayerChanged)
def currentLayer(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCurrentLayer()
return 0
return self._simulation_view.getCurrentLayer()
@pyqtProperty(int, notify=currentLayerChanged)
def minimumLayer(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinimumLayer()
return 0
return self._simulation_view.getMinimumLayer()
@pyqtProperty(int, notify=maxPathsChanged)
def numPaths(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxPaths()
return 0
return self._simulation_view.getMaxPaths()
@pyqtProperty(int, notify=currentPathChanged)
def currentPath(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCurrentPath()
return 0
return self._simulation_view.getCurrentPath()
@pyqtProperty(int, notify=currentPathChanged)
def minimumPath(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinimumPath()
return 0
return self._simulation_view.getMinimumPath()
@pyqtProperty(bool, notify=busyChanged)
def busy(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.isBusy()
return False
return self._simulation_view.isBusy()
@pyqtProperty(bool, notify=preferencesChanged)
def compatibilityMode(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getCompatibilityMode()
return False
return self._simulation_view.getCompatibilityMode()
@pyqtProperty(int, notify=globalStackChanged)
def extruderCount(self):
return self._simulation_view.getExtruderCount()
@pyqtSlot(int)
def setCurrentLayer(self, layer_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setLayer(layer_num)
self._simulation_view.setLayer(layer_num)
@pyqtSlot(int)
def setMinimumLayer(self, layer_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setMinimumLayer(layer_num)
self._simulation_view.setMinimumLayer(layer_num)
@pyqtSlot(int)
def setCurrentPath(self, path_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setPath(path_num)
self._simulation_view.setPath(path_num)
@pyqtSlot(int)
def setMinimumPath(self, path_num):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setMinimumPath(path_num)
self._simulation_view.setMinimumPath(path_num)
@pyqtSlot(int)
def setSimulationViewType(self, layer_view_type):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationViewType(layer_view_type)
self._simulation_view.setSimulationViewType(layer_view_type)
@pyqtSlot(result=int)
def getSimulationViewType(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getSimulationViewType()
return 0
return self._simulation_view.getSimulationViewType()
@pyqtSlot(bool)
def setSimulationRunning(self, running):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationRunning(running)
self._simulation_view.setSimulationRunning(running)
@pyqtSlot(result=bool)
def getSimulationRunning(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.isSimulationRunning()
return False
return self._simulation_view.isSimulationRunning()
@pyqtSlot(result=float)
def getMinFeedrate(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinFeedrate()
return 0
return self._simulation_view.getMinFeedrate()
@pyqtSlot(result=float)
def getMaxFeedrate(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxFeedrate()
return 0
return self._simulation_view.getMaxFeedrate()
@pyqtSlot(result=float)
def getMinThickness(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMinThickness()
return 0
return self._simulation_view.getMinThickness()
@pyqtSlot(result=float)
def getMaxThickness(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getMaxThickness()
return 0
return self._simulation_view.getMaxThickness()
# Opacity 0..1
@pyqtSlot(int, float)
def setExtruderOpacity(self, extruder_nr, opacity):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setExtruderOpacity(extruder_nr, opacity)
self._simulation_view.setExtruderOpacity(extruder_nr, opacity)
@pyqtSlot(int)
def setShowTravelMoves(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowTravelMoves(show)
self._simulation_view.setShowTravelMoves(show)
@pyqtSlot(int)
def setShowHelpers(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowHelpers(show)
self._simulation_view.setShowHelpers(show)
@pyqtSlot(int)
def setShowSkin(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowSkin(show)
self._simulation_view.setShowSkin(show)
@pyqtSlot(int)
def setShowInfill(self, show):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setShowInfill(show)
@pyqtProperty(int, notify=globalStackChanged)
def extruderCount(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
return active_view.getExtruderCount()
return 0
self._simulation_view.setShowInfill(show)
def _layerActivityChanged(self):
self.activityChanged.emit()
@ -236,24 +169,25 @@ class SimulationViewProxy(QObject):
def _onActiveViewChanged(self):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
# remove other connection if once the SimulationView was created.
if self.is_simulationView_selected:
active_view.currentLayerNumChanged.disconnect(self._onLayerChanged)
active_view.currentPathNumChanged.disconnect(self._onPathChanged)
active_view.maxLayersChanged.disconnect(self._onMaxLayersChanged)
active_view.maxPathsChanged.disconnect(self._onMaxPathsChanged)
active_view.busyChanged.disconnect(self._onBusyChanged)
active_view.activityChanged.disconnect(self._onActivityChanged)
active_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
active_view.preferencesChanged.disconnect(self._onPreferencesChanged)
if active_view == self._simulation_view:
self._simulation_view.currentLayerNumChanged.connect(self._onLayerChanged)
self._simulation_view.currentPathNumChanged.connect(self._onPathChanged)
self._simulation_view.maxLayersChanged.connect(self._onMaxLayersChanged)
self._simulation_view.maxPathsChanged.connect(self._onMaxPathsChanged)
self._simulation_view.busyChanged.connect(self._onBusyChanged)
self._simulation_view.activityChanged.connect(self._onActivityChanged)
self._simulation_view.globalStackChanged.connect(self._onGlobalStackChanged)
self._simulation_view.preferencesChanged.connect(self._onPreferencesChanged)
self.is_simulationView_selected = True
active_view.currentLayerNumChanged.connect(self._onLayerChanged)
active_view.currentPathNumChanged.connect(self._onPathChanged)
active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
active_view.maxPathsChanged.connect(self._onMaxPathsChanged)
active_view.busyChanged.connect(self._onBusyChanged)
active_view.activityChanged.connect(self._onActivityChanged)
active_view.globalStackChanged.connect(self._onGlobalStackChanged)
active_view.preferencesChanged.connect(self._onPreferencesChanged)
elif self.is_simulationView_selected:
# Disconnect all of em again.
self.is_simulationView_selected = False
self._simulation_view.currentLayerNumChanged.disconnect(self._onLayerChanged)
self._simulation_view.currentPathNumChanged.disconnect(self._onPathChanged)
self._simulation_view.maxLayersChanged.disconnect(self._onMaxLayersChanged)
self._simulation_view.maxPathsChanged.disconnect(self._onMaxPathsChanged)
self._simulation_view.busyChanged.disconnect(self._onBusyChanged)
self._simulation_view.activityChanged.disconnect(self._onActivityChanged)
self._simulation_view.globalStackChanged.disconnect(self._onGlobalStackChanged)
self._simulation_view.preferencesChanged.disconnect(self._onPreferencesChanged)

View file

@ -10,9 +10,11 @@ ScrollView
clip: true
width: parent.width
height: parent.height
contentHeight: mainColumn.height
Column
{
id: mainColumn
width: base.width
spacing: UM.Theme.getSize("default_margin").height

View file

@ -331,7 +331,7 @@ Cura.MachineAction
Label
{
text: catalog.i18nc("@label", "Enter the IP address or hostname of your printer on the network.")
text: catalog.i18nc("@label", "Enter the IP address of your printer on the network.")
width: parent.width
wrapMode: Text.WordWrap
renderType: Text.NativeRendering

View file

@ -64,6 +64,7 @@ Item
visible: printJob
// FIXED-LINE-HEIGHT:
width: parent.width
height: parent.height
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
@ -241,11 +242,10 @@ Item
enabled: !contextMenuButton.enabled
}
// TODO: uncomment this tooltip as soon as the required firmware is released
// MonitorInfoBlurb
// {
// id: contextMenuDisabledInfo
// text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
// target: contextMenuButton
// }
MonitorInfoBlurb
{
id: contextMenuDisabledInfo
text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
target: contextMenuButton
}
}

View file

@ -250,13 +250,12 @@ Item
enabled: !contextMenuButton.enabled
}
// TODO: uncomment this tooltip as soon as the required firmware is released
// MonitorInfoBlurb
// {
// id: contextMenuDisabledInfo
// text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
// target: contextMenuButton
// }
MonitorInfoBlurb
{
id: contextMenuDisabledInfo
text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
target: contextMenuButton
}
CameraButton
{
@ -495,6 +494,25 @@ Item
implicitWidth: 96 * screenScaleFactor // TODO: Theme!
visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
onClicked: base.enabled ? overrideConfirmationDialog.open() : {}
enabled: OutputDevice.supportsPrintJobActions
}
// For cloud printing, add this mouse area over the disabled details button to indicate that it's not available
MouseArea
{
id: detailsButtonDisabledButtonArea
anchors.fill: detailsButton
hoverEnabled: detailsButton.visible && !detailsButton.enabled
onEntered: overrideButtonDisabledInfo.open()
onExited: overrideButtonDisabledInfo.close()
enabled: !detailsButton.enabled
}
MonitorInfoBlurb
{
id: overrideButtonDisabledInfo
text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
target: detailsButton
}
}

View file

@ -102,7 +102,6 @@ Item
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter
width: 600 * screenScaleFactor // TODO: Theme! (Should match column size)
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
@ -186,7 +185,14 @@ Item
}
printJob: modelData
}
model: OutputDevice.queuedPrintJobs
model:
{
if (OutputDevice.receivedData)
{
return OutputDevice.queuedPrintJobs
}
return [null, null]
}
spacing: 6 // TODO: Theme!
}
}

View file

@ -50,7 +50,14 @@ Component
MonitorCarousel
{
id: carousel
printers: OutputDevice.printers
printers:
{
if (OutputDevice.receivedData)
{
return OutputDevice.printers
}
return [null]
}
}
}

View file

@ -56,7 +56,7 @@ class CloudApiClient:
## Retrieves all the clusters for the user that is currently logged in.
# \param on_finished: The function to be called after the result is parsed.
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None:
url = "{}/clusters".format(self.CLUSTER_API_ROOT)
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, CloudClusterResponse)

View file

@ -42,20 +42,18 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# The interval with which the remote cluster is checked.
# We can do this relatively often as this API call is quite fast.
CHECK_CLUSTER_INTERVAL = 8.0 # seconds
CHECK_CLUSTER_INTERVAL = 10.0 # seconds
# Override the network response timeout in seconds after which we consider the device offline.
# For cloud this needs to be higher because the interval at which we check the status is higher as well.
NETWORK_RESPONSE_CONSIDER_OFFLINE = 15.0 # seconds
# The minimum version of firmware that support print job actions over cloud.
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.3.0")
# Signal triggered when the print jobs in the queue were changed.
printJobsChanged = pyqtSignal()
# Signal triggered when the selected printer in the UI should be changed.
activePrinterChanged = pyqtSignal()
# Notify can only use signals that are defined by the class that they are in, not inherited ones.
# Therefore we create a private signal used to trigger the printersChanged signal.
_clusterPrintersChanged = pyqtSignal()
_cloudClusterPrintersChanged = pyqtSignal()
## Creates a new cloud output device
# \param api_client: The client that will run the API calls
@ -89,7 +87,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._setInterfaceElements()
# Trigger the printersChanged signal when the private signal is triggered.
self.printersChanged.connect(self._clusterPrintersChanged)
self.printersChanged.connect(self._cloudClusterPrintersChanged)
# Keep server string of the last generated time to avoid updating models more than once for the same response
self._received_printers = None # type: Optional[List[ClusterPrinterStatus]]
@ -144,8 +142,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
## Called when the network data should be updated.
def _update(self) -> None:
super()._update()
if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
return # Avoid calling the cloud too often
if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL:
return # avoid calling the cloud too often
self._time_of_last_request = time()
if self._account.isLoggedIn:
self.setAuthenticationState(AuthState.Authenticated)
self._last_request_time = time()
@ -156,9 +155,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
## Method called when HTTP request to status endpoint is finished.
# Contains both printers and print jobs statuses in a single response.
def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
# Update all data from the cluster.
self._last_response_time = time()
if self._received_printers != status.printers:
self._responseReceived()
if status.printers != self._received_printers:
self._received_printers = status.printers
self._updatePrinters(status.printers)
if status.print_jobs != self._received_print_jobs:
@ -232,7 +230,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self.writeError.emit()
## Whether the printer that this output device represents supports print job actions via the cloud.
@pyqtProperty(bool, notify=_clusterPrintersChanged)
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
def supportsPrintJobActions(self) -> bool:
if not self._printers:
return False
@ -274,7 +272,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
@clusterData.setter
def clusterData(self, value: CloudClusterResponse) -> None:
self._cluster = value
## Gets the URL on which to monitor the cluster via the cloud.
@property
def clusterCloudUrl(self) -> str:

View file

@ -161,15 +161,17 @@ class CloudOutputDeviceManager:
self._connectToOutputDevice(device, active_machine)
elif local_network_key and device.matchesNetworkKey(local_network_key):
# Connect to it if we can match the local network key that was already present.
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
self._connectToOutputDevice(device, active_machine)
elif device.key in output_device_manager.getOutputDeviceIds():
# Remove device if it is not meant for the active machine.
output_device_manager.removeOutputDevice(device.key)
## Connects to an output device and makes sure it is registered in the output device manager.
@staticmethod
def _connectToOutputDevice(device: CloudOutputDevice, active_machine: GlobalStack) -> None:
def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None:
machine.setName(device.name)
machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
machine.setMetaDataEntry("group_name", device.name)
device.connect()
active_machine.addConfiguredConnectionType(device.connectionType.value)
machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)

View file

@ -9,7 +9,8 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate
from ..BaseModel import BaseModel
## Class representing a cloud cluster printer configuration
## Class representing a cloud cluster printer configuration
# Also used for representing slots in a Material Station (as from Cura's perspective these are the same).
class ClusterPrintCoreConfiguration(BaseModel):
## Creates a new cloud cluster printer configuration object
@ -18,7 +19,7 @@ class ClusterPrintCoreConfiguration(BaseModel):
# \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'.
# \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'.
def __init__(self, extruder_index: int,
material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial],
material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial] = None,
print_core_id: Optional[str] = None, **kwargs) -> None:
self.extruder_index = extruder_index
self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None

View file

@ -101,6 +101,7 @@ class ClusterPrintJobStatus(BaseModel):
extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()]
configuration = PrinterConfigurationModel()
configuration.setExtruderConfigurations(extruders)
configuration.setPrinterType(self.machine_variant)
return configuration
## Updates an UM3 print job output model based on this cloud cluster print job.

View file

@ -0,0 +1,23 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Union, Dict, Any, List
from ..BaseModel import BaseModel
from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot
## Class representing the data of a Material Station in the cluster.
class ClusterPrinterMaterialStation(BaseModel):
## Creates a new Material Station status.
# \param status: The status of the material station.
# \param: supported: Whether the material station is supported on this machine or not.
# \param material_slots: The active slots configurations of this material station.
def __init__(self, status: str, supported: bool = False,
material_slots: Union[None, Dict[str, Any], ClusterPrinterMaterialStationSlot] = None,
**kwargs) -> None:
self.status = status
self.supported = supported
self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\
if material_slots else [] # type: List[ClusterPrinterMaterialStationSlot]
super().__init__(**kwargs)

View file

@ -0,0 +1,17 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration
## Class representing the data of a single slot in the material station.
class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration):
## Create a new material station slot object.
# \param slot_index: The index of the slot in the material station (ranging 0 to 5).
# \param compatible: Whether the configuration is compatible with the print core.
# \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data).
def __init__(self, slot_index: int, compatible: bool, material_remaining: float, **kwargs):
self.slot_index = slot_index
self.compatible = compatible
self.material_remaining = material_remaining
super().__init__(**kwargs)

View file

@ -1,14 +1,18 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from itertools import product
from typing import List, Union, Dict, Optional, Any
from PyQt5.QtCore import QUrl
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from .ClusterBuildPlate import ClusterBuildPlate
from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration
from .ClusterPrinterMaterialStation import ClusterPrinterMaterialStation
from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot
from ..BaseModel import BaseModel
@ -26,17 +30,19 @@ class ClusterPrinterStatus(BaseModel):
# \param uuid: The unique ID of the printer, also known as GUID.
# \param configuration: The active print core configurations of this printer.
# \param reserved_by: A printer can be claimed by a specific print job.
# \param maintenance_required: Indicates if maintenance is necessary
# \param maintenance_required: Indicates if maintenance is necessary.
# \param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date",
# "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible"
# \param latest_available_firmware: The version of the latest firmware that is available
# \param build_plate: The build plate that is on the printer
# "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible".
# \param latest_available_firmware: The version of the latest firmware that is available.
# \param build_plate: The build plate that is on the printer.
# \param material_station: The material station that is on the printer.
def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str,
status: str, unique_name: str, uuid: str,
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]],
reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None,
firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None,
build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, **kwargs) -> None:
build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None,
material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None:
self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration)
self.enabled = enabled
@ -52,6 +58,8 @@ class ClusterPrinterStatus(BaseModel):
self.firmware_update_status = firmware_update_status
self.latest_available_firmware = latest_available_firmware
self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None
self.material_station = self.parseModel(ClusterPrinterMaterialStation,
material_station) if material_station else None
super().__init__(**kwargs)
## Creates a new output model.
@ -71,8 +79,53 @@ class ClusterPrinterStatus(BaseModel):
model.updateBuildplate(self.build_plate.type if self.build_plate else "glass")
model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address)))
if model.printerConfiguration is not None:
for configuration, extruder_output, extruder_config in \
zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations):
configuration.updateOutputModel(extruder_output)
configuration.updateConfigurationModel(extruder_config)
# Set the possible configurations based on whether a Material Station is present or not.
if self.material_station is not None and len(self.material_station.material_slots):
self._updateAvailableConfigurations(model)
if self.configuration is not None:
self._updateActiveConfiguration(model)
def _updateActiveConfiguration(self, model: PrinterOutputModel) -> None:
configurations = zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations)
for configuration, extruder_output, extruder_config in configurations:
configuration.updateOutputModel(extruder_output)
configuration.updateConfigurationModel(extruder_config)
def _updateAvailableConfigurations(self, model: PrinterOutputModel) -> None:
# Generate a list of configurations for the left extruder.
left_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration(
slot = slot,
extruder_index = 0
)]
# Generate a list of configurations for the right extruder.
right_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration(
slot = slot,
extruder_index = 1
)]
# Create a list of all available combinations between both print cores.
available_configurations = [self._createAvailableConfigurationFromPrinterConfiguration(
left_slot = left_slot,
right_slot = right_slot,
printer_configuration = model.printerConfiguration
) for left_slot, right_slot in product(left_configurations, right_configurations)]
# Let Cura know which available configurations there are.
model.setAvailableConfigurations(available_configurations)
## Check if a configuration is supported in order to make it selectable by the user.
# We filter out any slot that is not supported by the extruder index, print core type or if the material is empty.
@staticmethod
def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool:
return slot.extruder_index == extruder_index and slot.compatible and slot.material and \
slot.material_remaining != 0
@staticmethod
def _createAvailableConfigurationFromPrinterConfiguration(left_slot: ClusterPrinterMaterialStationSlot,
right_slot: ClusterPrinterMaterialStationSlot,
printer_configuration: PrinterConfigurationModel
) -> PrinterConfigurationModel:
available_configuration = PrinterConfigurationModel()
available_configuration.setExtruderConfigurations([left_slot.createConfigurationModel(),
right_slot.createConfigurationModel()])
available_configuration.setPrinterType(printer_configuration.printerType)
available_configuration.setBuildplateConfiguration(printer_configuration.buildplateConfiguration)
return available_configuration

View file

@ -62,6 +62,11 @@ class ClusterApiClient:
def movePrintJobToTop(self, print_job_uuid: str) -> None:
url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode())
## Override print job configuration and force it to be printed.
def forcePrintJob(self, print_job_uuid: str) -> None:
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.put(self._createEmptyRequest(url), json.dumps({"force": True}).encode())
## Delete a print job from the queue.
def deletePrintJob(self, print_job_uuid: str) -> None:

View file

@ -38,16 +38,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
parent=parent
)
# API client for making requests to the print cluster.
self._cluster_api = ClusterApiClient(address, on_error=lambda error: print(error))
self._cluster_api = None # type: Optional[ClusterApiClient]
# We don't have authentication over local networking, so we're always authenticated.
self.setAuthenticationState(AuthState.Authenticated)
self._setInterfaceElements()
self._active_camera_url = QUrl() # type: QUrl
# Get the printers of this cluster to check if this device is a group host or not.
self._cluster_api.getPrinters(self._updatePrinters)
## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None:
self.setPriority(3) # Make sure the output device gets selected above local file output
@ -81,26 +78,26 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
@pyqtSlot(str, name="sendJobToTop")
def sendJobToTop(self, print_job_uuid: str) -> None:
self._cluster_api.movePrintJobToTop(print_job_uuid)
self._getApiClient().movePrintJobToTop(print_job_uuid)
@pyqtSlot(str, name="deleteJobFromQueue")
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
self._cluster_api.deletePrintJob(print_job_uuid)
self._getApiClient().deletePrintJob(print_job_uuid)
@pyqtSlot(str, name="forceSendJob")
def forceSendJob(self, print_job_uuid: str) -> None:
pass # TODO
self._getApiClient().forcePrintJob(print_job_uuid)
## Set the remote print job state.
# \param print_job_uuid: The UUID of the print job to set the state for.
# \param action: The action to undertake ('pause', 'resume', 'abort').
def setJobState(self, print_job_uuid: str, action: str) -> None:
self._cluster_api.setPrintJobState(print_job_uuid, action)
self._getApiClient().setPrintJobState(print_job_uuid, action)
def _update(self) -> None:
super()._update()
self._cluster_api.getPrinters(self._updatePrinters)
self._cluster_api.getPrintJobs(self._updatePrintJobs)
self._getApiClient().getPrinters(self._updatePrinters)
self._getApiClient().getPrintJobs(self._updatePrintJobs)
self._updatePrintJobPreviewImages()
## Sync the material profiles in Cura with the printer.
@ -162,4 +159,10 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
def _updatePrintJobPreviewImages(self):
for print_job in self._print_jobs:
if print_job.getPreviewImage() is None:
self._cluster_api.getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData)
self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData)
## Get the API client instance.
def _getApiClient(self) -> ClusterApiClient:
if not self._cluster_api:
self._cluster_api = ClusterApiClient(self.address, on_error=lambda error: print(error))
return self._cluster_api

View file

@ -236,7 +236,7 @@ class LocalClusterOutputDeviceManager:
machine.setName(device.name)
machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key)
machine.setMetaDataEntry("group_name", device.name)
device.connect()
machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)

View file

@ -18,7 +18,7 @@ I18N_CATALOG = i18nCatalog("cura")
## Machine action that allows to connect the active machine to a networked devices.
# TODO: in the future this should be part of the new discovery workflow baked into Cura.
class UltimakerNetworkedPrinterAction(MachineAction):
# Signal emitted when discovered devices have changed.
discoveredDevicesChanged = pyqtSignal()
@ -34,58 +34,54 @@ class UltimakerNetworkedPrinterAction(MachineAction):
## Start listening to network discovery events via the plugin.
@pyqtSlot(name = "startDiscovery")
def startDiscovery(self) -> None:
network_plugin = self._getNetworkPlugin()
network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self.discoveredDevicesChanged.emit() # trigger at least once to populate the list
## Reset the discovered devices.
@pyqtSlot(name = "reset")
def reset(self) -> None:
self.restartDiscovery()
self.discoveredDevicesChanged.emit() # trigger to reset the list
## Reset the discovered devices.
@pyqtSlot(name = "restartDiscovery")
def restartDiscovery(self) -> None:
network_plugin = self._getNetworkPlugin()
network_plugin.startDiscovery()
self._networkPlugin.startDiscovery()
self.discoveredDevicesChanged.emit() # trigger to reset the list
## Remove a manually added device.
@pyqtSlot(str, str, name = "removeManualDevice")
def removeManualDevice(self, key: str, address: str) -> None:
network_plugin = self._getNetworkPlugin()
network_plugin.removeManualDevice(key, address)
self._networkPlugin.removeManualDevice(key, address)
## Add a new manual device. Can replace an existing one by key.
@pyqtSlot(str, str, name = "setManualDevice")
def setManualDevice(self, key: str, address: str) -> None:
network_plugin = self._getNetworkPlugin()
if key != "":
network_plugin.removeManualDevice(key)
self._networkPlugin.removeManualDevice(key)
if address != "":
network_plugin.addManualDevice(address)
self._networkPlugin.addManualDevice(address)
## Get the devices discovered in the local network sorted by name.
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
def foundDevices(self):
network_plugin = self._getNetworkPlugin()
discovered_devices = list(network_plugin.getDiscoveredDevices().values())
discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values())
discovered_devices.sort(key = lambda d: d.name)
return discovered_devices
## Connect a device selected in the list with the active machine.
@pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice")
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
network_plugin = self._getNetworkPlugin()
network_plugin.associateActiveMachineWithPrinterDevice(device)
self._networkPlugin.associateActiveMachineWithPrinterDevice(device)
## Callback for when the list of discovered devices in the plugin was changed.
def _onDeviceDiscoveryChanged(self) -> None:
self.discoveredDevicesChanged.emit()
## Get the network manager from the plugin.
def _getNetworkPlugin(self) -> UM3OutputDevicePlugin:
@property
def _networkPlugin(self) -> UM3OutputDevicePlugin:
if not self._network_plugin:
plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin = cast(UM3OutputDevicePlugin, plugin)
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin = cast(UM3OutputDevicePlugin, network_plugin)
return self._network_plugin

View file

@ -26,7 +26,7 @@ from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
# Currently used for local networking and cloud printing using Ultimaker Connect.
# This base class primarily contains all the Qt properties and slots needed for the monitor page to work.
class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
META_NETWORK_KEY = "um_network_key"
META_CLUSTER_ID = "um_cloud_cluster_id"
@ -42,21 +42,23 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
# States indicating if a print job is queued.
QUEUED_PRINT_JOBS_STATES = {"queued", "error"}
# Time in seconds since last network response after which we consider this device offline.
# We set this a bit higher than some of the other intervals to make sure they don't overlap.
NETWORK_RESPONSE_CONSIDER_OFFLINE = 12.0
NETWORK_RESPONSE_CONSIDER_OFFLINE = 10.0 # seconds
def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType,
parent=None) -> None:
super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type,
parent=parent)
# Trigger the printersChanged signal when the private signal is triggered.
self.printersChanged.connect(self._clusterPrintersChanged)
# Keeps track the last network response to determine if we are still connected.
self._time_of_last_response = time()
self._time_of_last_request = time()
# Set the display name from the properties
self.setName(self.getProperty("name"))
@ -101,15 +103,18 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
return [print_job for print_job in self._print_jobs if
print_job.assignedPrinter is not None and print_job.state not in self.QUEUED_PRINT_JOBS_STATES]
@pyqtProperty(bool, notify=printJobsChanged)
def receivedPrintJobs(self) -> bool:
return bool(self._print_jobs)
@pyqtProperty(bool, notify=_clusterPrintersChanged)
def receivedData(self) -> bool:
return self._has_received_printers
# Get the amount of printers in the cluster.
@pyqtProperty(int, notify=_clusterPrintersChanged)
def clusterSize(self) -> int:
if not self._has_received_printers:
return 1 # prevent false positives when discovering new devices
discovered_size = self.getProperty("cluster_size")
if discovered_size == "":
return 1 # prevent false positives for new devices
return int(discovered_size)
return len(self._printers)
# Get the amount of printer in the cluster per type.
@ -294,6 +299,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
print_job_data.updateOutputModel(print_job)
if print_job_data.printer_uuid:
self._updateAssignedPrinter(print_job, print_job_data.printer_uuid)
if print_job_data.assigned_to:
self._updateAssignedPrinter(print_job, print_job_data.assigned_to)
new_print_jobs.append(print_job)
# Check which print job need to be removed (de-referenced).
@ -312,6 +319,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
model = remote_job.createOutputModel(ClusterOutputController(self))
if remote_job.printer_uuid:
self._updateAssignedPrinter(model, remote_job.printer_uuid)
if remote_job.assigned_to:
self._updateAssignedPrinter(model, remote_job.assigned_to)
return model
## Updates the printer assignment for the given print job model.

View file

@ -14,6 +14,40 @@ _renamed_profiles = {"generic_pla_0.4_coarse": "jbo_generic_pla_0.4_coarse",
"generic_petg_0.4_medium": "jbo_generic_petg_medium",
}
# - The variant "imade3d jellybox 0.4 mm 2-fans" for machine definition "imade3d_jellybox"
# is now "0.4 mm" for machine definition "imade3d jellybox_2".
# - Materials "imade3d_petg_green" and "imade3d_petg_pink" are now "imade3d_petg_175".
# - Materials "imade3d_pla_green" and "imade3d_pla_pink" are now "imade3d_petg_175".
#
# Note: Theoretically, the old material profiles with "_2-fans" at the end should be updated to:
# - machine definition: imade3d_jellybox_2
# - variant: 0.4 mm (for jellybox 2)
# - material: (as an example) imade3d_petg_175_imade3d_jellybox_2_0.4_mm
#
# But this involves changing the definition of the global stack and the extruder stacks, which can cause more trouble
# than what we can fix. So, here, we update all material variants, regardless of having "_2-fans" at the end or not, to
# jellybox_0.4_mm.
#
_renamed_material_profiles = { # PETG
"imade3d_petg_green": "imade3d_petg_175",
"imade3d_petg_green_imade3d_jellybox": "imade3d_petg_175_imade3d_jellybox",
"imade3d_petg_green_imade3d_jellybox_0.4_mm": "imade3d_petg_175_imade3d_jellybox_0.4_mm",
"imade3d_petg_green_imade3d_jellybox_0.4_mm_2-fans": "imade3d_petg_175_imade3d_jellybox_0.4_mm",
"imade3d_petg_pink": "imade3d_petg_175",
"imade3d_petg_pink_imade3d_jellybox": "imade3d_petg_175_imade3d_jellybox",
"imade3d_petg_pink_imade3d_jellybox_0.4_mm": "imade3d_petg_175_imade3d_jellybox_0.4_mm",
"imade3d_petg_pink_imade3d_jellybox_0.4_mm_2-fans": "imade3d_petg_175_imade3d_jellybox_0.4_mm",
# PLA
"imade3d_pla_green": "imade3d_pla_175",
"imade3d_pla_green_imade3d_jellybox": "imade3d_pla_175_imade3d_jellybox",
"imade3d_pla_green_imade3d_jellybox_0.4_mm": "imade3d_pla_175_imade3d_jellybox_0.4_mm",
"imade3d_pla_green_imade3d_jellybox_0.4_mm_2-fans": "imade3d_pla_175_imade3d_jellybox_0.4_mm",
"imade3d_pla_pink": "imade3d_pla_175",
"imade3d_pla_pink_imade3d_jellybox": "imade3d_pla_175_imade3d_jellybox",
"imade3d_pla_pink_imade3d_jellybox_0.4_mm": "imade3d_pla_175_imade3d_jellybox_0.4_mm",
"imade3d_pla_pink_imade3d_jellybox_0.4_mm_2-fans": "imade3d_pla_175_imade3d_jellybox_0.4_mm",
}
_removed_settings = {
"start_layers_at_same_position"
}
@ -114,8 +148,8 @@ class VersionUpgrade42to43(VersionUpgrade):
parser["containers"]["2"] = _renamed_profiles[parser["containers"]["2"]]
material_id = parser["containers"]["3"]
if material_id.endswith("_2-fans"):
parser["containers"]["3"] = material_id.replace("_2-fans", "")
if material_id in _renamed_material_profiles:
parser["containers"]["3"] = _renamed_material_profiles[material_id]
variant_id = parser["containers"]["4"]
if variant_id.endswith("_2-fans"):