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

@ -12,9 +12,10 @@ import json
import ssl
import urllib.request
import urllib.error
import shutil
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QUrl
import certifi
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
from PyQt5.QtGui import QDesktopServices
@ -22,7 +23,6 @@ from UM.Application import Application
from UM.Logger import Logger
from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog
from UM.Platform import Platform
from UM.Resources import Resources
catalog = i18nCatalog("cura")
@ -352,11 +352,13 @@ class CrashHandler:
# Convert data to bytes
binary_data = json.dumps(self.data).encode("utf-8")
# 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())
# Submit data
kwoptions = {"data": binary_data, "timeout": 5}
if Platform.isOSX():
kwoptions["context"] = ssl._create_unverified_context()
kwoptions = {"data": binary_data,
"timeout": 5,
"context": context}
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
if not self.has_started:

View file

@ -75,7 +75,7 @@ class DiscoveredPrinter(QObject):
def readableMachineType(self) -> str:
from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager()
# In LocalClusterOutputDevice, when it updates a printer information, it updates the machine type using the field
# In NetworkOutputDevice, when it updates a printer information, it updates the machine type using the field
# "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
# like "Ultimaker 3". The code below handles this case.
if self._hasHumanReadableMachineTypeName(self._machine_type):

View file

@ -2,15 +2,19 @@
# Cura is released under the terms of the LGPLv3 or higher.
import json
import webbrowser
from datetime import datetime, timedelta
import os
from typing import Optional, TYPE_CHECKING
from urllib.parse import urlencode
import requests.exceptions
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.Signal import Signal
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
@ -163,7 +167,7 @@ class AuthorizationService:
})
# Open the authorization page in a new browser window.
webbrowser.open_new("{}?{}".format(self._auth_url, query_string))
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
# Start a local web server to receive the callback URL on.
self._server.start(verification_code)

View file

@ -25,7 +25,7 @@ class ExtruderConfigurationModel(QObject):
return self._position
def setMaterial(self, material: Optional[MaterialOutputModel]) -> None:
if self._hotend_id != material:
if self._material != material:
self._material = material
self.extruderConfigurationChanged.emit()
@ -33,7 +33,7 @@ class ExtruderConfigurationModel(QObject):
def activeMaterial(self) -> Optional[MaterialOutputModel]:
return self._material
@pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged)
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
def material(self) -> Optional[MaterialOutputModel]:
return self._material

View file

@ -58,6 +58,14 @@ class PrinterConfigurationModel(QObject):
return False
return self._printer_type != ""
def hasAnyMaterialLoaded(self) -> bool:
if not self.isValid():
return False
for configuration in self._extruder_configurations:
if configuration.activeMaterial and configuration.activeMaterial.type != "empty":
return True
return False
def __str__(self):
message_chunks = []
message_chunks.append("Printer type: " + self._printer_type)

View file

@ -7,6 +7,7 @@ from UM.Math.Vector import Vector
from cura.PrinterOutput.Peripheral import Peripheral
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
from UM.Logger import Logger
if TYPE_CHECKING:
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
@ -37,7 +38,7 @@ class PrinterOutputModel(QObject):
self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
self._active_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version
@ -47,8 +48,10 @@ class PrinterOutputModel(QObject):
self._buildplate = ""
self._peripherals = [] # type: List[Peripheral]
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
self._active_printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
self._extruders]
self._active_printer_configuration.configurationChanged.connect(self.configurationChanged)
self._available_printer_configurations = [] # type: List[PrinterConfigurationModel]
self._camera_url = QUrl() # type: QUrl
@ -81,7 +84,7 @@ class PrinterOutputModel(QObject):
def updateType(self, printer_type: str) -> None:
if self._printer_type != printer_type:
self._printer_type = printer_type
self._printer_configuration.printerType = self._printer_type
self._active_printer_configuration.printerType = self._printer_type
self.typeChanged.emit()
self.configurationChanged.emit()
@ -92,7 +95,7 @@ class PrinterOutputModel(QObject):
def updateBuildplate(self, buildplate: str) -> None:
if self._buildplate != buildplate:
self._buildplate = buildplate
self._printer_configuration.buildplateConfiguration = self._buildplate
self._active_printer_configuration.buildplateConfiguration = self._buildplate
self.buildplateChanged.emit()
self.configurationChanged.emit()
@ -290,18 +293,18 @@ class PrinterOutputModel(QObject):
def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.canUpdateFirmwareChanged.emit()
# Returns the configuration (material, variant and buildplate) of the current printer
# Returns the active configuration (material, variant and buildplate) of the current printer
@pyqtProperty(QObject, notify = configurationChanged)
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
if self._printer_configuration.isValid():
return self._printer_configuration
if self._active_printer_configuration.isValid():
return self._active_printer_configuration
return None
peripheralsChanged = pyqtSignal()
@pyqtProperty(str, notify = peripheralsChanged)
def peripherals(self) -> str:
return ", ".join(*[peripheral.name for peripheral in self._peripherals])
return ", ".join([peripheral.name for peripheral in self._peripherals])
def addPeripheral(self, peripheral: Peripheral) -> None:
self._peripherals.append(peripheral)
@ -310,3 +313,28 @@ class PrinterOutputModel(QObject):
def removePeripheral(self, peripheral: Peripheral) -> None:
self._peripherals.remove(peripheral)
self.peripheralsChanged.emit()
availableConfigurationsChanged = pyqtSignal()
# The availableConfigurations are configuration options that a printer can switch to, but doesn't currently have
# active (eg; Automatic tool changes, material loaders, etc).
@pyqtProperty("QVariantList", notify = availableConfigurationsChanged)
def availableConfigurations(self) -> List[PrinterConfigurationModel]:
return self._available_printer_configurations
def addAvailableConfiguration(self, new_configuration: PrinterConfigurationModel) -> None:
if new_configuration not in self._available_printer_configurations:
self._available_printer_configurations.append(new_configuration)
self.availableConfigurationsChanged.emit()
def removeAvailableConfiguration(self, config_to_remove: PrinterConfigurationModel) -> None:
try:
self._available_printer_configurations.remove(config_to_remove)
except ValueError:
Logger.log("w", "Unable to remove configuration that isn't in the list of available configurations")
else:
self.availableConfigurationsChanged.emit()
def setAvailableConfigurations(self, new_configurations: List[PrinterConfigurationModel]) -> None:
self._available_printer_configurations = new_configurations
self.availableConfigurationsChanged.emit()

View file

@ -220,20 +220,25 @@ class PrinterOutputDevice(QObject, OutputDevice):
return self._unique_configurations
def _updateUniqueConfigurations(self) -> None:
self._unique_configurations = sorted(
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
key=lambda config: config.printerType,
)
all_configurations = set()
for printer in self._printers:
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
all_configurations.add(printer.printerConfiguration)
all_configurations.update(printer.availableConfigurations)
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
if new_configurations != self._unique_configurations:
self._unique_configurations = new_configurations
self.uniqueConfigurationsChanged.emit()
# Returns the unique configurations of the printers within this output device
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
def uniquePrinterTypes(self) -> List[str]:
return list(sorted(set([configuration.printerType for configuration in self._unique_configurations])))
return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
def _onPrintersChanged(self) -> None:
for printer in self._printers:
printer.configurationChanged.connect(self._updateUniqueConfigurations)
printer.availableConfigurationsChanged.connect(self._updateUniqueConfigurations)
# At this point there may be non-updated configurations
self._updateUniqueConfigurations()

View file

@ -9,7 +9,6 @@ from typing import Any, cast, Dict, Optional, List, Union
from PyQt5.QtWidgets import QMessageBox
from UM.Decorators import override
from UM.PluginObject import PluginObject
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.Interfaces import ContainerInterface
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -21,7 +20,6 @@ from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
from UM.Util import parseBool
from UM.Resources import Resources
from cura.ReaderWriters.ProfileWriter import ProfileWriter
@ -29,6 +27,7 @@ from . import ExtruderStack
from . import GlobalStack
import cura.CuraApplication
from cura.Settings.cura_empty_instance_containers import empty_quality_container
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
@ -389,13 +388,33 @@ class CuraContainerRegistry(ContainerRegistry):
# successfully imported but then fail to show up.
quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
if quality_type not in quality_group_dict:
# "not_supported" profiles can be imported.
if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict:
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
ContainerRegistry.getInstance().addContainer(profile)
return None
@override(ContainerRegistry)
def saveDirtyContainers(self) -> None:
# Lock file for "more" atomically loading and saving to/from config dir.
with self.lockFile():
# Save base files first
for instance in self.findDirtyContainers(container_type=InstanceContainer):
if instance.getMetaDataEntry("removed"):
continue
if instance.getId() == instance.getMetaData().get("base_file"):
self.saveContainer(instance)
for instance in self.findDirtyContainers(container_type=InstanceContainer):
if instance.getMetaDataEntry("removed"):
continue
self.saveContainer(instance)
for stack in self.findContainerStacks():
self.saveContainer(stack)
## Gets a list of profile writer plugins
# \return List of tuples of (plugin_id, meta_data).
def _getIOPlugins(self, io_type):

View file

@ -60,6 +60,14 @@ if Platform.isWindows() and hasattr(sys, "frozen"):
except KeyError:
pass
# GITHUB issue #6194: https://github.com/Ultimaker/Cura/issues/6194
# With AppImage 2 on Linux, the current working directory will be somewhere in /tmp/<rand>/usr, which is owned
# by root. For some reason, QDesktopServices.openUrl() requires to have a usable current working directory,
# otherwise it doesn't work. This is a workaround on Linux that before we call QDesktopServices.openUrl(), we
# switch to a directory where the user has the ownership.
if Platform.isLinux() and hasattr(sys, "frozen"):
os.chdir(os.path.expanduser("~"))
# WORKAROUND: GITHUB-704 GITHUB-708
# It looks like setuptools creates a .pth file in
# the default /usr/lib which causes the default site-packages

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

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

@ -10,6 +10,7 @@ from ..BaseModel import BaseModel
## 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):
# 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

@ -63,6 +63,11 @@ class ClusterApiClient:
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:
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)

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

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

@ -45,10 +45,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
# 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)
@ -57,6 +58,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
# 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"):

View file

@ -107,8 +107,8 @@
"cool_fan_enabled": { "value": true },
"cool_min_layer_time": { "value": 10 },
"adhesion_type": { "value": "'none' if support_enable else 'skirt'" },
"brim_replaces_support": { "value": false},
"adhesion_type": { "value": "'skirt'" },
"brim_replaces_support": { "value": false },
"skirt_gap": { "value": 10.0 },
"skirt_line_count": { "value": 4 },

View file

@ -2,6 +2,11 @@
"name": "Creality Ender-3",
"version": 2,
"inherits": "creality_base",
"metadata": {
"quality_definition": "creality_base",
"visible": true,
"platform": "creality_ender3.stl"
},
"overrides": {
"machine_name": { "default_value": "Creality Ender-3" },
"machine_width": { "default_value": 220 },
@ -23,10 +28,5 @@
},
"gantry_height": { "value": 25 }
},
"metadata": {
"quality_definition": "creality_base",
"visible": true
}
}

View file

@ -205,6 +205,16 @@
"settable_per_extruder": false,
"settable_per_meshgroup": false
},
"machine_heated_build_volume":
{
"label": "Has Build Volume Temperature Stabilization",
"description": "Whether the machine is able to stabilize the build volume temperature.",
"default_value": false,
"type": "bool",
"settable_per_mesh": false,
"settable_per_extruder": false,
"settable_per_meshgroup": false
},
"machine_center_is_zero":
{
"label": "Is Center Origin",
@ -2088,7 +2098,7 @@
"default_value": 210,
"minimum_value_warning": "0",
"maximum_value_warning": "285",
"enabled": "machine_nozzle_temp_enabled",
"enabled": false,
"settable_per_extruder": true,
"settable_per_mesh": false,
"minimum_value": "-273.15"
@ -2104,7 +2114,7 @@
"minimum_value": "-273.15",
"minimum_value_warning": "0",
"maximum_value_warning": "285",
"enabled": true,
"enabled": "machine_heated_build_volume",
"settable_per_mesh": false,
"settable_per_extruder": false
},
@ -2191,9 +2201,9 @@
"resolve": "max(extruderValues('default_material_bed_temperature'))",
"default_value": 60,
"minimum_value": "-273.15",
"minimum_value_warning": "0",
"minimum_value_warning": "build_volume_temperature",
"maximum_value_warning": "130",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"",
"enabled": false,
"settable_per_mesh": false,
"settable_per_extruder": false,
"settable_per_meshgroup": false
@ -2208,7 +2218,7 @@
"value": "default_material_bed_temperature",
"resolve": "max(extruderValues('material_bed_temperature'))",
"minimum_value": "-273.15",
"minimum_value_warning": "0",
"minimum_value_warning": "build_volume_temperature",
"maximum_value_warning": "130",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"",
"settable_per_mesh": false,
@ -2225,7 +2235,7 @@
"default_value": 60,
"value": "resolveOrValue('material_bed_temperature')",
"minimum_value": "-273.15",
"minimum_value_warning": "max(extruderValues('material_bed_temperature'))",
"minimum_value_warning": "max(build_volume_temperature, max(extruderValues('material_bed_temperature')))",
"maximum_value_warning": "130",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"",
"settable_per_mesh": false,

View file

@ -0,0 +1,93 @@
{
"name": "JGAurora A3S",
"version": 2,
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Samuel Pinches",
"manufacturer": "JGAurora",
"file_formats": "text/x-gcode",
"preferred_quality_type": "normal",
"machine_extruder_trains":
{
"0": "jgaurora_a3s_extruder_0"
}
},
"overrides": {
"machine_name": {
"default_value": "JGAurora A3S"
},
"machine_start_gcode": {
"default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --"
},
"machine_end_gcode": {
"default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y200 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --"
},
"machine_width": {
"default_value": 205
},
"machine_height": {
"default_value": 205
},
"machine_depth": {
"default_value": 205
},
"machine_heated_bed": {
"default_value": true
},
"machine_center_is_zero": {
"default_value": false
},
"gantry_height": {
"value": "10"
},
"machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)"
},
"material_diameter": {
"default_value": 1.75
},
"material_print_temperature": {
"default_value": 210
},
"material_bed_temperature": {
"default_value": 65
},
"layer_height_0": {
"default_value": 0.12
},
"wall_thickness": {
"default_value": 1.2
},
"speed_print": {
"default_value": 35
},
"speed_infill": {
"default_value": 40
},
"speed_wall": {
"default_value": 30
},
"speed_topbottom": {
"default_value": 20
},
"speed_travel": {
"default_value": 100
},
"speed_layer_0": {
"default_value": 12
},
"support_enable": {
"default_value": true
},
"retraction_enable": {
"default_value": true
},
"retraction_amount": {
"default_value": 8
},
"retraction_speed": {
"default_value": 45
}
}
}

View file

@ -30,7 +30,7 @@
"id": 9066,
"check_urls":
[
"http://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version"
"https://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version"
],
"update_url": "https://ultimaker.com/firmware"
}

View file

@ -27,7 +27,7 @@
"id": 9511,
"check_urls":
[
"http://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version"
"https://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version"
],
"update_url": "https://ultimaker.com/firmware"
}

View file

@ -33,7 +33,7 @@
"weight": -2,
"firmware_update_info": {
"id": 9051,
"check_urls": ["http://software.ultimaker.com/releases/firmware/9051/stable/um-update.swu.version"],
"check_urls": ["https://software.ultimaker.com/releases/firmware/9051/stable/um-update.swu.version"],
"update_url": "https://ultimaker.com/firmware"
}
},
@ -44,6 +44,7 @@
"machine_depth": { "default_value": 240 },
"machine_height": { "default_value": 300 },
"machine_heated_bed": { "default_value": true },
"machine_heated_build_volume": { "default_value": true },
"machine_nozzle_heat_up_speed": { "default_value": 1.4 },
"machine_nozzle_cool_down_speed": { "default_value": 0.8 },
"machine_head_with_fans_polygon":

View file

@ -0,0 +1,16 @@
{
"id": "jgaurora_a3s_extruder_0",
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata": {
"machine": "jgaurora_a3s",
"position": "0"
},
"overrides": {
"extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View file

@ -1223,7 +1223,7 @@ msgstr "Imprimer les parties du modèle qui sont horizontalement plus fines que
#: fdmprinter.def.json
msgctxt "xy_offset label"
msgid "Horizontal Expansion"
msgstr "Vitesse dimpression horizontale"
msgstr "Expansion horizontale"
#: fdmprinter.def.json
msgctxt "xy_offset description"

Binary file not shown.

View file

@ -112,7 +112,7 @@ SettingItem
return UM.Theme.getColor("setting_validation_warning");
}
// Validation is OK.
if (control.containsMouse || control.activeFocus)
if (control.containsMouse || control.activeFocus || hovered)
{
return UM.Theme.getColor("setting_control_border_highlight")
}

View file

@ -12,7 +12,6 @@ SettingItem
{
id: base
property var focusItem: control
contents: Cura.ComboBox
{
id: control
@ -21,6 +20,7 @@ SettingItem
textRole: "value"
anchors.fill: parent
highlighted: base.hovered
onActivated:
{

View file

@ -99,7 +99,7 @@ Item
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
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.")
}
Item

View file

@ -14,7 +14,7 @@ import Cura 1.1 as Cura
ComboBox
{
id: control
property bool highlighted: False
background: Rectangle
{
color:
@ -24,7 +24,7 @@ ComboBox
return UM.Theme.getColor("setting_control_disabled")
}
if (control.hovered || control.activeFocus)
if (control.hovered || control.activeFocus || control.highlighted)
{
return UM.Theme.getColor("setting_control_highlight")
}
@ -41,7 +41,7 @@ ComboBox
return UM.Theme.getColor("setting_control_disabled_border")
}
if (control.hovered || control.activeFocus)
if (control.hovered || control.activeFocus || control.highlighted)
{
return UM.Theme.getColor("setting_control_border_highlight")
}

View file

@ -5,11 +5,14 @@ from unittest.mock import MagicMock
import pytest
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Peripheral import Peripheral
test_validate_data_get_set = [
{"attribute": "name", "value": "YAY"},
{"attribute": "targetBedTemperature", "value": 192},
{"attribute": "cameraUrl", "value": "YAY!"}
]
test_validate_data_get_update = [
@ -22,6 +25,7 @@ test_validate_data_get_update = [
{"attribute": "targetBedTemperature", "value": 9001},
{"attribute": "activePrintJob", "value": PrintJobOutputModel(MagicMock())},
{"attribute": "state", "value": "BEEPBOOP"},
]
@ -79,3 +83,67 @@ def test_getAndUpdate(data):
getattr(model, "update" + attribute)(data["value"])
# The signal should not fire again
assert signal.emit.call_count == 1
def test_peripherals():
model = PrinterOutputModel(MagicMock())
model.peripheralsChanged = MagicMock()
peripheral = MagicMock(spec=Peripheral)
peripheral.name = "test"
peripheral2 = MagicMock(spec=Peripheral)
peripheral2.name = "test2"
model.addPeripheral(peripheral)
assert model.peripheralsChanged.emit.call_count == 1
model.addPeripheral(peripheral2)
assert model.peripheralsChanged.emit.call_count == 2
assert model.peripherals == "test, test2"
model.removePeripheral(peripheral)
assert model.peripheralsChanged.emit.call_count == 3
assert model.peripherals == "test2"
def test_availableConfigurations_addConfiguration():
model = PrinterOutputModel(MagicMock())
configuration = MagicMock(spec = PrinterConfigurationModel)
model.addAvailableConfiguration(configuration)
assert model.availableConfigurations == [configuration]
def test_availableConfigurations_addConfigTwice():
model = PrinterOutputModel(MagicMock())
configuration = MagicMock(spec=PrinterConfigurationModel)
model.setAvailableConfigurations([configuration])
assert model.availableConfigurations == [configuration]
# Adding it again should not have any effect
model.addAvailableConfiguration(configuration)
assert model.availableConfigurations == [configuration]
def test_availableConfigurations_removeConfig():
model = PrinterOutputModel(MagicMock())
configuration = MagicMock(spec=PrinterConfigurationModel)
model.addAvailableConfiguration(configuration)
model.removeAvailableConfiguration(configuration)
assert model.availableConfigurations == []
def test_removeAlreadyRemovedConfiguration():
model = PrinterOutputModel(MagicMock())
configuration = MagicMock(spec=PrinterConfigurationModel)
model.availableConfigurationsChanged = MagicMock()
model.removeAvailableConfiguration(configuration)
assert model.availableConfigurationsChanged.emit.call_count == 0
assert model.availableConfigurations == []

View file

@ -1,6 +1,12 @@
from unittest.mock import MagicMock
import pytest
from unittest.mock import patch
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
test_validate_data_get_set = [
@ -8,10 +14,15 @@ test_validate_data_get_set = [
{"attribute": "connectionState", "value": 1},
]
@pytest.fixture()
def printer_output_device():
with patch("UM.Application.Application.getInstance"):
return PrinterOutputDevice("whatever")
@pytest.mark.parametrize("data", test_validate_data_get_set)
def test_getAndSet(data):
model = PrinterOutputDevice("whatever")
def test_getAndSet(data, printer_output_device):
model = printer_output_device
# Convert the first letter into a capital
attribute = list(data["attribute"])
@ -35,3 +46,43 @@ def test_getAndSet(data):
getattr(model, "set" + attribute)(data["value"])
# The signal should not fire again
assert signal.emit.call_count == 1
def test_uniqueConfigurations(printer_output_device):
printer = PrinterOutputModel(MagicMock())
# Add a printer and fire the signal that ensures they get hooked up correctly.
printer_output_device._printers = [printer]
printer_output_device._onPrintersChanged()
assert printer_output_device.uniqueConfigurations == []
configuration = PrinterConfigurationModel()
printer.addAvailableConfiguration(configuration)
assert printer_output_device.uniqueConfigurations == [configuration]
# Once the type of printer is set, it's active configuration counts as being set.
# In that case, that should also be added to the list of available configurations
printer.updateType("blarg!")
loaded_material = MaterialOutputModel(guid = "", type = "PLA", color = "Blue", brand = "Generic", name = "Blue PLA")
loaded_left_extruder = ExtruderConfigurationModel(0)
loaded_left_extruder.setMaterial(loaded_material)
loaded_right_extruder = ExtruderConfigurationModel(1)
loaded_right_extruder.setMaterial(loaded_material)
printer.printerConfiguration.setExtruderConfigurations([loaded_left_extruder, loaded_right_extruder])
assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration]
def test_uniqueConfigurations_empty_is_filtered_out(printer_output_device):
printer = PrinterOutputModel(MagicMock())
# Add a printer and fire the signal that ensures they get hooked up correctly.
printer_output_device._printers = [printer]
printer_output_device._onPrintersChanged()
printer.updateType("blarg!")
empty_material = MaterialOutputModel(guid = "", type = "empty", color = "empty", brand = "Generic", name = "Empty")
empty_left_extruder = ExtruderConfigurationModel(0)
empty_left_extruder.setMaterial(empty_material)
empty_right_extruder = ExtruderConfigurationModel(1)
empty_right_extruder.setMaterial(empty_material)
printer.printerConfiguration.setExtruderConfigurations([empty_left_extruder, empty_right_extruder])
assert printer_output_device.uniqueConfigurations == []

View file

@ -1,9 +1,10 @@
import webbrowser
from datetime import datetime
from unittest.mock import MagicMock, patch
import requests
from PyQt5.QtGui import QDesktopServices
from UM.Preferences import Preferences
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
from cura.OAuth2.AuthorizationService import AuthorizationService
@ -172,12 +173,12 @@ def test_storeAuthData(get_user_profile) -> None:
@patch.object(LocalAuthorizationServer, "stop")
@patch.object(LocalAuthorizationServer, "start")
@patch.object(webbrowser, "open_new")
def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None:
@patch.object(QDesktopServices, "openUrl")
def test_localAuthServer(QDesktopServices_openUrl, start_auth_server, stop_auth_server) -> None:
preferences = Preferences()
authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
authorization_service.startAuthorizationFlow()
assert webbrowser_open.call_count == 1
assert QDesktopServices_openUrl.call_count == 1
# Ensure that the Authorization service tried to start the server.
assert start_auth_server.call_count == 1