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

View file

@ -75,7 +75,7 @@ class DiscoveredPrinter(QObject):
def readableMachineType(self) -> str: def readableMachineType(self) -> str:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager() 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 # "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. # like "Ultimaker 3". The code below handles this case.
if self._hasHumanReadableMachineTypeName(self._machine_type): if self._hasHumanReadableMachineTypeName(self._machine_type):

View file

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

View file

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

View file

@ -58,6 +58,14 @@ class PrinterConfigurationModel(QObject):
return False return False
return self._printer_type != "" 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): def __str__(self):
message_chunks = [] message_chunks = []
message_chunks.append("Printer type: " + self._printer_type) 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.Peripheral import Peripheral
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
from UM.Logger import Logger
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
@ -37,7 +38,7 @@ class PrinterOutputModel(QObject):
self._controller = output_controller self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged) self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] 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._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel] self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version self._firmware_version = firmware_version
@ -47,8 +48,10 @@ class PrinterOutputModel(QObject):
self._buildplate = "" self._buildplate = ""
self._peripherals = [] # type: List[Peripheral] 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._extruders]
self._active_printer_configuration.configurationChanged.connect(self.configurationChanged)
self._available_printer_configurations = [] # type: List[PrinterConfigurationModel]
self._camera_url = QUrl() # type: QUrl self._camera_url = QUrl() # type: QUrl
@ -81,7 +84,7 @@ class PrinterOutputModel(QObject):
def updateType(self, printer_type: str) -> None: def updateType(self, printer_type: str) -> None:
if self._printer_type != printer_type: if self._printer_type != printer_type:
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.typeChanged.emit()
self.configurationChanged.emit() self.configurationChanged.emit()
@ -92,7 +95,7 @@ class PrinterOutputModel(QObject):
def updateBuildplate(self, buildplate: str) -> None: def updateBuildplate(self, buildplate: str) -> None:
if self._buildplate != buildplate: if self._buildplate != buildplate:
self._buildplate = buildplate self._buildplate = buildplate
self._printer_configuration.buildplateConfiguration = self._buildplate self._active_printer_configuration.buildplateConfiguration = self._buildplate
self.buildplateChanged.emit() self.buildplateChanged.emit()
self.configurationChanged.emit() self.configurationChanged.emit()
@ -290,18 +293,18 @@ class PrinterOutputModel(QObject):
def _onControllerCanUpdateFirmwareChanged(self) -> None: def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.canUpdateFirmwareChanged.emit() 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) @pyqtProperty(QObject, notify = configurationChanged)
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]: def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
if self._printer_configuration.isValid(): if self._active_printer_configuration.isValid():
return self._printer_configuration return self._active_printer_configuration
return None return None
peripheralsChanged = pyqtSignal() peripheralsChanged = pyqtSignal()
@pyqtProperty(str, notify = peripheralsChanged) @pyqtProperty(str, notify = peripheralsChanged)
def peripherals(self) -> str: 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: def addPeripheral(self, peripheral: Peripheral) -> None:
self._peripherals.append(peripheral) self._peripherals.append(peripheral)
@ -309,4 +312,29 @@ class PrinterOutputModel(QObject):
def removePeripheral(self, peripheral: Peripheral) -> None: def removePeripheral(self, peripheral: Peripheral) -> None:
self._peripherals.remove(peripheral) self._peripherals.remove(peripheral)
self.peripheralsChanged.emit() 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 return self._unique_configurations
def _updateUniqueConfigurations(self) -> None: def _updateUniqueConfigurations(self) -> None:
self._unique_configurations = sorted( all_configurations = set()
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None}, for printer in self._printers:
key=lambda config: config.printerType, if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
) all_configurations.add(printer.printerConfiguration)
self.uniqueConfigurationsChanged.emit() 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 # Returns the unique configurations of the printers within this output device
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged) @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
def uniquePrinterTypes(self) -> List[str]: 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: def _onPrintersChanged(self) -> None:
for printer in self._printers: for printer in self._printers:
printer.configurationChanged.connect(self._updateUniqueConfigurations) printer.configurationChanged.connect(self._updateUniqueConfigurations)
printer.availableConfigurationsChanged.connect(self._updateUniqueConfigurations)
# At this point there may be non-updated configurations # At this point there may be non-updated configurations
self._updateUniqueConfigurations() self._updateUniqueConfigurations()

View file

@ -9,7 +9,6 @@ from typing import Any, cast, Dict, Optional, List, Union
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.Decorators import override from UM.Decorators import override
from UM.PluginObject import PluginObject
from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
@ -21,7 +20,6 @@ from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Platform import Platform from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. 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 UM.Resources import Resources
from cura.ReaderWriters.ProfileWriter import ProfileWriter from cura.ReaderWriters.ProfileWriter import ProfileWriter
@ -29,6 +27,7 @@ from . import ExtruderStack
from . import GlobalStack from . import GlobalStack
import cura.CuraApplication import cura.CuraApplication
from cura.Settings.cura_empty_instance_containers import empty_quality_container
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
@ -389,13 +388,33 @@ class CuraContainerRegistry(ContainerRegistry):
# successfully imported but then fail to show up. # successfully imported but then fail to show up.
quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack) 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) return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
ContainerRegistry.getInstance().addContainer(profile) ContainerRegistry.getInstance().addContainer(profile)
return None 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 ## Gets a list of profile writer plugins
# \return List of tuples of (plugin_id, meta_data). # \return List of tuples of (plugin_id, meta_data).
def _getIOPlugins(self, io_type): def _getIOPlugins(self, io_type):

View file

@ -60,6 +60,14 @@ if Platform.isWindows() and hasattr(sys, "frozen"):
except KeyError: except KeyError:
pass 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 # WORKAROUND: GITHUB-704 GITHUB-708
# It looks like setuptools creates a .pth file in # It looks like setuptools creates a .pth file in
# the default /usr/lib which causes the default site-packages # the default /usr/lib which causes the default site-packages

View file

@ -59,6 +59,9 @@ class MachineInfo:
self.container_id = None self.container_id = None
self.name = None self.name = None
self.definition_id = None self.definition_id = None
self.metadata_dict = {} # type: Dict[str, str]
self.quality_type = None self.quality_type = None
self.custom_quality_name = None self.custom_quality_name = None
self.quality_changes_info = None self.quality_changes_info = None
@ -342,6 +345,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack_id = self._stripFileToId(global_stack_file) global_stack_id = self._stripFileToId(global_stack_file)
serialized = archive.open(global_stack_file).read().decode("utf-8") serialized = archive.open(global_stack_file).read().decode("utf-8")
machine_name = self._getMachineNameFromSerializedStack(serialized) machine_name = self._getMachineNameFromSerializedStack(serialized)
self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized)
stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine") stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine")
self._is_same_machine_type = True self._is_same_machine_type = True
existing_global_stack = None existing_global_stack = None
@ -832,7 +837,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.quality_changes_info.name = quality_changes_name self._machine_info.quality_changes_info.name = quality_changes_name
def _clearStack(self, stack): @staticmethod
def _clearStack(stack):
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
stack.definitionChanges.clear() stack.definitionChanges.clear()
@ -978,6 +984,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack.setMetaDataEntry("enabled", "True") extruder_stack.setMetaDataEntry("enabled", "True")
extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled)) 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): def _updateActiveMachine(self, global_stack):
# Actually change the active machine. # Actually change the active machine.
machine_manager = Application.getInstance().getMachineManager() machine_manager = Application.getInstance().getMachineManager()
@ -986,6 +997,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
machine_manager.setActiveMachine(global_stack.getId()) 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: if self._quality_changes_to_apply:
quality_changes_group_dict = quality_manager.getQualityChangesGroups(global_stack) quality_changes_group_dict = quality_manager.getQualityChangesGroups(global_stack)
if self._quality_changes_to_apply not in quality_changes_group_dict: 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. # Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop()) global_stack.containersChanged.emit(global_stack.getTop())
def _stripFileToId(self, file): @staticmethod
def _stripFileToId(file):
mime_type = MimeTypeDatabase.getMimeTypeForFile(file) mime_type = MimeTypeDatabase.getMimeTypeForFile(file)
file = mime_type.stripExtension(file) file = mime_type.stripExtension(file)
return file.replace("Cura/", "") return file.replace("Cura/", "")
@ -1021,7 +1038,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) 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. ## 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 = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized) parser.read_string(serialized)
@ -1042,12 +1060,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return container_ids return container_ids
def _getMachineNameFromSerializedStack(self, serialized): @staticmethod
def _getMachineNameFromSerializedStack(serialized):
parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized) parser.read_string(serialized)
return parser["general"].get("name", "") 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) data = ET.fromstring(serialized)
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"}) metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
for entry in metadata: 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. # \param message The protobuf message containing g-code, encoded as UTF-8.
def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None: 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. ## Called when a g-code prefix message is received from the engine.
# #
# \param message The protobuf message containing the g-code prefix, # \param message The protobuf message containing the g-code prefix,
# encoded as UTF-8. # encoded as UTF-8.
def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: 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. ## Creates a new socket connection.
def _createSocket(self, protocol_file: str = None) -> None: def _createSocket(self, protocol_file: str = None) -> None:

View file

@ -10,6 +10,9 @@ from UM.Version import Version
import urllib.request import urllib.request
from urllib.error import URLError from urllib.error import URLError
from typing import Dict, Optional from typing import Dict, Optional
import ssl
import certifi
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
@ -39,8 +42,12 @@ class FirmwareUpdateCheckerJob(Job):
result = self.STRING_ZERO_VERSION result = self.STRING_ZERO_VERSION
try: 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) 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") result = response.read().decode("utf-8")
except URLError: except URLError:
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url)) 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 # 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. # notify the user when no new firmware version is available.
if (checked_version != "") and (checked_version != current_version): 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, message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name,
self._lookups.getRedirectUserUrl()) self._lookups.getRedirectUserUrl())
message.actionTriggered.connect(self._callback) 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)) Logger.log("i", "No machine with name {0} in list of firmware to check.".format(self._machine_name))
except Exception as e: 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: if not self.silent:
Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show() Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show()
return return

View file

@ -142,6 +142,18 @@ Item
forceUpdateOnChangeFunction: forceUpdateFunction 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" Cura.ComboBoxWithOptions // "G-code flavor"
{ {
id: gcodeFlavorComboBox id: gcodeFlavorComboBox

View file

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

View file

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

View file

@ -331,7 +331,7 @@ Cura.MachineAction
Label 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 width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
renderType: Text.NativeRendering renderType: Text.NativeRendering

View file

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

View file

@ -250,13 +250,12 @@ Item
enabled: !contextMenuButton.enabled enabled: !contextMenuButton.enabled
} }
// TODO: uncomment this tooltip as soon as the required firmware is released MonitorInfoBlurb
// MonitorInfoBlurb {
// { id: contextMenuDisabledInfo
// id: contextMenuDisabledInfo text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
// text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.") target: contextMenuButton
// target: contextMenuButton }
// }
CameraButton CameraButton
{ {
@ -495,6 +494,25 @@ Item
implicitWidth: 96 * screenScaleFactor // TODO: Theme! implicitWidth: 96 * screenScaleFactor // TODO: Theme!
visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
onClicked: base.enabled ? overrideConfirmationDialog.open() : {} 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 elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 600 * screenScaleFactor // TODO: Theme! (Should match column size)
// FIXED-LINE-HEIGHT: // FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
@ -186,7 +185,14 @@ Item
} }
printJob: modelData printJob: modelData
} }
model: OutputDevice.queuedPrintJobs model:
{
if (OutputDevice.receivedData)
{
return OutputDevice.queuedPrintJobs
}
return [null, null]
}
spacing: 6 // TODO: Theme! spacing: 6 // TODO: Theme!
} }
} }

View file

@ -50,7 +50,14 @@ Component
MonitorCarousel MonitorCarousel
{ {
id: carousel 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. ## 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. # \param on_finished: The function to be called after the result is parsed.
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None: 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)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, CloudClusterResponse) self._addCallback(reply, on_finished, CloudClusterResponse)

View file

@ -42,20 +42,18 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# The interval with which the remote cluster is checked. # The interval with which the remote cluster is checked.
# We can do this relatively often as this API call is quite fast. # 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. # The minimum version of firmware that support print job actions over cloud.
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.3.0") 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. # 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. # Therefore we create a private signal used to trigger the printersChanged signal.
_clusterPrintersChanged = pyqtSignal() _cloudClusterPrintersChanged = pyqtSignal()
## Creates a new cloud output device ## Creates a new cloud output device
# \param api_client: The client that will run the API calls # \param api_client: The client that will run the API calls
@ -89,7 +87,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._setInterfaceElements() self._setInterfaceElements()
# Trigger the printersChanged signal when the private signal is triggered. # 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 # 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]] self._received_printers = None # type: Optional[List[ClusterPrinterStatus]]
@ -144,8 +142,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
## Called when the network data should be updated. ## Called when the network data should be updated.
def _update(self) -> None: def _update(self) -> None:
super()._update() super()._update()
if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL: if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL:
return # Avoid calling the cloud too often return # avoid calling the cloud too often
self._time_of_last_request = time()
if self._account.isLoggedIn: if self._account.isLoggedIn:
self.setAuthenticationState(AuthState.Authenticated) self.setAuthenticationState(AuthState.Authenticated)
self._last_request_time = time() self._last_request_time = time()
@ -156,9 +155,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
## Method called when HTTP request to status endpoint is finished. ## Method called when HTTP request to status endpoint is finished.
# Contains both printers and print jobs statuses in a single response. # Contains both printers and print jobs statuses in a single response.
def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
# Update all data from the cluster. self._responseReceived()
self._last_response_time = time() if status.printers != self._received_printers:
if self._received_printers != status.printers:
self._received_printers = status.printers self._received_printers = status.printers
self._updatePrinters(status.printers) self._updatePrinters(status.printers)
if status.print_jobs != self._received_print_jobs: if status.print_jobs != self._received_print_jobs:
@ -232,7 +230,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self.writeError.emit() self.writeError.emit()
## Whether the printer that this output device represents supports print job actions via the cloud. ## 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: def supportsPrintJobActions(self) -> bool:
if not self._printers: if not self._printers:
return False return False
@ -274,7 +272,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
@clusterData.setter @clusterData.setter
def clusterData(self, value: CloudClusterResponse) -> None: def clusterData(self, value: CloudClusterResponse) -> None:
self._cluster = value self._cluster = value
## Gets the URL on which to monitor the cluster via the cloud. ## Gets the URL on which to monitor the cluster via the cloud.
@property @property
def clusterCloudUrl(self) -> str: def clusterCloudUrl(self) -> str:

View file

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

View file

@ -9,7 +9,8 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate
from ..BaseModel import BaseModel 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): class ClusterPrintCoreConfiguration(BaseModel):
## Creates a new cloud cluster printer configuration object ## 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 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'. # \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'.
def __init__(self, extruder_index: int, 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: print_core_id: Optional[str] = None, **kwargs) -> None:
self.extruder_index = extruder_index self.extruder_index = extruder_index
self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None 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 ()] extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()]
configuration = PrinterConfigurationModel() configuration = PrinterConfigurationModel()
configuration.setExtruderConfigurations(extruders) configuration.setExtruderConfigurations(extruders)
configuration.setPrinterType(self.machine_variant)
return configuration return configuration
## Updates an UM3 print job output model based on this cloud cluster print job. ## 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. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from itertools import product
from typing import List, Union, Dict, Optional, Any from typing import List, Union, Dict, Optional, Any
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from .ClusterBuildPlate import ClusterBuildPlate from .ClusterBuildPlate import ClusterBuildPlate
from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration
from .ClusterPrinterMaterialStation import ClusterPrinterMaterialStation
from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
@ -26,17 +30,19 @@ class ClusterPrinterStatus(BaseModel):
# \param uuid: The unique ID of the printer, also known as GUID. # \param uuid: The unique ID of the printer, also known as GUID.
# \param configuration: The active print core configurations of this printer. # \param configuration: The active print core configurations of this printer.
# \param reserved_by: A printer can be claimed by a specific print job. # \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", # \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" # "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 latest_available_firmware: The version of the latest firmware that is available.
# \param build_plate: The build plate that is on the printer # \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, def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str,
status: str, unique_name: str, uuid: str, status: str, unique_name: str, uuid: str,
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]],
reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None, reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None,
firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = 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.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration)
self.enabled = enabled self.enabled = enabled
@ -52,6 +58,8 @@ class ClusterPrinterStatus(BaseModel):
self.firmware_update_status = firmware_update_status self.firmware_update_status = firmware_update_status
self.latest_available_firmware = latest_available_firmware self.latest_available_firmware = latest_available_firmware
self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None 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) super().__init__(**kwargs)
## Creates a new output model. ## 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.updateBuildplate(self.build_plate.type if self.build_plate else "glass")
model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address)))
if model.printerConfiguration is not None: # Set the possible configurations based on whether a Material Station is present or not.
for configuration, extruder_output, extruder_config in \ if self.material_station is not None and len(self.material_station.material_slots):
zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations): self._updateAvailableConfigurations(model)
configuration.updateOutputModel(extruder_output) if self.configuration is not None:
configuration.updateConfigurationModel(extruder_config) 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: def movePrintJobToTop(self, print_job_uuid: str) -> None:
url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid) 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()) 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. ## Delete a print job from the queue.
def deletePrintJob(self, print_job_uuid: str) -> None: def deletePrintJob(self, print_job_uuid: str) -> None:

View file

@ -38,16 +38,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
parent=parent parent=parent
) )
# API client for making requests to the print cluster. self._cluster_api = None # type: Optional[ClusterApiClient]
self._cluster_api = ClusterApiClient(address, on_error=lambda error: print(error))
# We don't have authentication over local networking, so we're always authenticated. # We don't have authentication over local networking, so we're always authenticated.
self.setAuthenticationState(AuthState.Authenticated) self.setAuthenticationState(AuthState.Authenticated)
self._setInterfaceElements() self._setInterfaceElements()
self._active_camera_url = QUrl() # type: QUrl 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. ## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None: def _setInterfaceElements(self) -> None:
self.setPriority(3) # Make sure the output device gets selected above local file output 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") @pyqtSlot(str, name="sendJobToTop")
def sendJobToTop(self, print_job_uuid: str) -> None: 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") @pyqtSlot(str, name="deleteJobFromQueue")
def deleteJobFromQueue(self, print_job_uuid: str) -> None: 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") @pyqtSlot(str, name="forceSendJob")
def forceSendJob(self, print_job_uuid: str) -> None: def forceSendJob(self, print_job_uuid: str) -> None:
pass # TODO self._getApiClient().forcePrintJob(print_job_uuid)
## Set the remote print job state. ## Set the remote print job state.
# \param print_job_uuid: The UUID of the print job to set the state for. # \param print_job_uuid: The UUID of the print job to set the state for.
# \param action: The action to undertake ('pause', 'resume', 'abort'). # \param action: The action to undertake ('pause', 'resume', 'abort').
def setJobState(self, print_job_uuid: str, action: str) -> None: 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: def _update(self) -> None:
super()._update() super()._update()
self._cluster_api.getPrinters(self._updatePrinters) self._getApiClient().getPrinters(self._updatePrinters)
self._cluster_api.getPrintJobs(self._updatePrintJobs) self._getApiClient().getPrintJobs(self._updatePrintJobs)
self._updatePrintJobPreviewImages() self._updatePrintJobPreviewImages()
## Sync the material profiles in Cura with the printer. ## Sync the material profiles in Cura with the printer.
@ -162,4 +159,10 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
def _updatePrintJobPreviewImages(self): def _updatePrintJobPreviewImages(self):
for print_job in self._print_jobs: for print_job in self._print_jobs:
if print_job.getPreviewImage() is None: 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.setName(device.name)
machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key) machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key)
machine.setMetaDataEntry("group_name", device.name) machine.setMetaDataEntry("group_name", device.name)
device.connect() device.connect()
machine.addConfiguredConnectionType(device.connectionType.value) machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) 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. ## 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. # TODO: in the future this should be part of the new discovery workflow baked into Cura.
class UltimakerNetworkedPrinterAction(MachineAction): class UltimakerNetworkedPrinterAction(MachineAction):
# Signal emitted when discovered devices have changed. # Signal emitted when discovered devices have changed.
discoveredDevicesChanged = pyqtSignal() discoveredDevicesChanged = pyqtSignal()
@ -34,58 +34,54 @@ class UltimakerNetworkedPrinterAction(MachineAction):
## Start listening to network discovery events via the plugin. ## Start listening to network discovery events via the plugin.
@pyqtSlot(name = "startDiscovery") @pyqtSlot(name = "startDiscovery")
def startDiscovery(self) -> None: def startDiscovery(self) -> None:
network_plugin = self._getNetworkPlugin() self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self.discoveredDevicesChanged.emit() # trigger at least once to populate the list self.discoveredDevicesChanged.emit() # trigger at least once to populate the list
## Reset the discovered devices. ## Reset the discovered devices.
@pyqtSlot(name = "reset") @pyqtSlot(name = "reset")
def reset(self) -> None: def reset(self) -> None:
self.restartDiscovery() self.discoveredDevicesChanged.emit() # trigger to reset the list
## Reset the discovered devices. ## Reset the discovered devices.
@pyqtSlot(name = "restartDiscovery") @pyqtSlot(name = "restartDiscovery")
def restartDiscovery(self) -> None: def restartDiscovery(self) -> None:
network_plugin = self._getNetworkPlugin() self._networkPlugin.startDiscovery()
network_plugin.startDiscovery()
self.discoveredDevicesChanged.emit() # trigger to reset the list self.discoveredDevicesChanged.emit() # trigger to reset the list
## Remove a manually added device. ## Remove a manually added device.
@pyqtSlot(str, str, name = "removeManualDevice") @pyqtSlot(str, str, name = "removeManualDevice")
def removeManualDevice(self, key: str, address: str) -> None: def removeManualDevice(self, key: str, address: str) -> None:
network_plugin = self._getNetworkPlugin() self._networkPlugin.removeManualDevice(key, address)
network_plugin.removeManualDevice(key, address)
## Add a new manual device. Can replace an existing one by key. ## Add a new manual device. Can replace an existing one by key.
@pyqtSlot(str, str, name = "setManualDevice") @pyqtSlot(str, str, name = "setManualDevice")
def setManualDevice(self, key: str, address: str) -> None: def setManualDevice(self, key: str, address: str) -> None:
network_plugin = self._getNetworkPlugin()
if key != "": if key != "":
network_plugin.removeManualDevice(key) self._networkPlugin.removeManualDevice(key)
if address != "": if address != "":
network_plugin.addManualDevice(address) self._networkPlugin.addManualDevice(address)
## Get the devices discovered in the local network sorted by name. ## Get the devices discovered in the local network sorted by name.
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged) @pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
def foundDevices(self): def foundDevices(self):
network_plugin = self._getNetworkPlugin() discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values())
discovered_devices = list(network_plugin.getDiscoveredDevices().values())
discovered_devices.sort(key = lambda d: d.name) discovered_devices.sort(key = lambda d: d.name)
return discovered_devices return discovered_devices
## Connect a device selected in the list with the active machine. ## Connect a device selected in the list with the active machine.
@pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice") @pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice")
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
network_plugin = self._getNetworkPlugin() self._networkPlugin.associateActiveMachineWithPrinterDevice(device)
network_plugin.associateActiveMachineWithPrinterDevice(device)
## Callback for when the list of discovered devices in the plugin was changed. ## Callback for when the list of discovered devices in the plugin was changed.
def _onDeviceDiscoveryChanged(self) -> None: def _onDeviceDiscoveryChanged(self) -> None:
self.discoveredDevicesChanged.emit() self.discoveredDevicesChanged.emit()
## Get the network manager from the plugin. ## Get the network manager from the plugin.
def _getNetworkPlugin(self) -> UM3OutputDevicePlugin: @property
def _networkPlugin(self) -> UM3OutputDevicePlugin:
if not self._network_plugin: if not self._network_plugin:
plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
self._network_plugin = cast(UM3OutputDevicePlugin, plugin) network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin = cast(UM3OutputDevicePlugin, network_plugin)
return self._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. # 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. # This base class primarily contains all the Qt properties and slots needed for the monitor page to work.
class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
META_NETWORK_KEY = "um_network_key" META_NETWORK_KEY = "um_network_key"
META_CLUSTER_ID = "um_cloud_cluster_id" META_CLUSTER_ID = "um_cloud_cluster_id"
@ -42,21 +42,23 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
# States indicating if a print job is queued. # States indicating if a print job is queued.
QUEUED_PRINT_JOBS_STATES = {"queued", "error"} QUEUED_PRINT_JOBS_STATES = {"queued", "error"}
# Time in seconds since last network response after which we consider this device offline. # 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. # 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, def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType,
parent=None) -> None: parent=None) -> None:
super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type,
parent=parent) parent=parent)
# Trigger the printersChanged signal when the private signal is triggered. # Trigger the printersChanged signal when the private signal is triggered.
self.printersChanged.connect(self._clusterPrintersChanged) self.printersChanged.connect(self._clusterPrintersChanged)
# Keeps track the last network response to determine if we are still connected. # Keeps track the last network response to determine if we are still connected.
self._time_of_last_response = time() self._time_of_last_response = time()
self._time_of_last_request = time()
# Set the display name from the properties # Set the display name from the properties
self.setName(self.getProperty("name")) self.setName(self.getProperty("name"))
@ -101,15 +103,18 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
return [print_job for print_job in self._print_jobs if 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] print_job.assignedPrinter is not None and print_job.state not in self.QUEUED_PRINT_JOBS_STATES]
@pyqtProperty(bool, notify=printJobsChanged) @pyqtProperty(bool, notify=_clusterPrintersChanged)
def receivedPrintJobs(self) -> bool: def receivedData(self) -> bool:
return bool(self._print_jobs) return self._has_received_printers
# Get the amount of printers in the cluster. # Get the amount of printers in the cluster.
@pyqtProperty(int, notify=_clusterPrintersChanged) @pyqtProperty(int, notify=_clusterPrintersChanged)
def clusterSize(self) -> int: def clusterSize(self) -> int:
if not self._has_received_printers: 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) return len(self._printers)
# Get the amount of printer in the cluster per type. # Get the amount of printer in the cluster per type.
@ -294,6 +299,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
print_job_data.updateOutputModel(print_job) print_job_data.updateOutputModel(print_job)
if print_job_data.printer_uuid: if print_job_data.printer_uuid:
self._updateAssignedPrinter(print_job, 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) new_print_jobs.append(print_job)
# Check which print job need to be removed (de-referenced). # Check which print job need to be removed (de-referenced).
@ -312,6 +319,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
model = remote_job.createOutputModel(ClusterOutputController(self)) model = remote_job.createOutputModel(ClusterOutputController(self))
if remote_job.printer_uuid: if remote_job.printer_uuid:
self._updateAssignedPrinter(model, 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 return model
## Updates the printer assignment for the given print job 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", "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 = { _removed_settings = {
"start_layers_at_same_position" "start_layers_at_same_position"
} }
@ -114,8 +148,8 @@ class VersionUpgrade42to43(VersionUpgrade):
parser["containers"]["2"] = _renamed_profiles[parser["containers"]["2"]] parser["containers"]["2"] = _renamed_profiles[parser["containers"]["2"]]
material_id = parser["containers"]["3"] material_id = parser["containers"]["3"]
if material_id.endswith("_2-fans"): if material_id in _renamed_material_profiles:
parser["containers"]["3"] = material_id.replace("_2-fans", "") parser["containers"]["3"] = _renamed_material_profiles[material_id]
variant_id = parser["containers"]["4"] variant_id = parser["containers"]["4"]
if variant_id.endswith("_2-fans"): if variant_id.endswith("_2-fans"):

View file

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

View file

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

View file

@ -205,6 +205,16 @@
"settable_per_extruder": false, "settable_per_extruder": false,
"settable_per_meshgroup": 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": "machine_center_is_zero":
{ {
"label": "Is Center Origin", "label": "Is Center Origin",
@ -2088,7 +2098,7 @@
"default_value": 210, "default_value": 210,
"minimum_value_warning": "0", "minimum_value_warning": "0",
"maximum_value_warning": "285", "maximum_value_warning": "285",
"enabled": "machine_nozzle_temp_enabled", "enabled": false,
"settable_per_extruder": true, "settable_per_extruder": true,
"settable_per_mesh": false, "settable_per_mesh": false,
"minimum_value": "-273.15" "minimum_value": "-273.15"
@ -2104,7 +2114,7 @@
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "0", "minimum_value_warning": "0",
"maximum_value_warning": "285", "maximum_value_warning": "285",
"enabled": true, "enabled": "machine_heated_build_volume",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false "settable_per_extruder": false
}, },
@ -2191,9 +2201,9 @@
"resolve": "max(extruderValues('default_material_bed_temperature'))", "resolve": "max(extruderValues('default_material_bed_temperature'))",
"default_value": 60, "default_value": 60,
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "0", "minimum_value_warning": "build_volume_temperature",
"maximum_value_warning": "130", "maximum_value_warning": "130",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "enabled": false,
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false, "settable_per_extruder": false,
"settable_per_meshgroup": false "settable_per_meshgroup": false
@ -2208,7 +2218,7 @@
"value": "default_material_bed_temperature", "value": "default_material_bed_temperature",
"resolve": "max(extruderValues('material_bed_temperature'))", "resolve": "max(extruderValues('material_bed_temperature'))",
"minimum_value": "-273.15", "minimum_value": "-273.15",
"minimum_value_warning": "0", "minimum_value_warning": "build_volume_temperature",
"maximum_value_warning": "130", "maximum_value_warning": "130",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -2225,7 +2235,7 @@
"default_value": 60, "default_value": 60,
"value": "resolveOrValue('material_bed_temperature')", "value": "resolveOrValue('material_bed_temperature')",
"minimum_value": "-273.15", "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", "maximum_value_warning": "130",
"enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"",
"settable_per_mesh": false, "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, "id": 9066,
"check_urls": "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" "update_url": "https://ultimaker.com/firmware"
} }

View file

@ -27,7 +27,7 @@
"id": 9511, "id": 9511,
"check_urls": "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" "update_url": "https://ultimaker.com/firmware"
} }

View file

@ -33,7 +33,7 @@
"weight": -2, "weight": -2,
"firmware_update_info": { "firmware_update_info": {
"id": 9051, "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" "update_url": "https://ultimaker.com/firmware"
} }
}, },
@ -44,6 +44,7 @@
"machine_depth": { "default_value": 240 }, "machine_depth": { "default_value": 240 },
"machine_height": { "default_value": 300 }, "machine_height": { "default_value": 300 },
"machine_heated_bed": { "default_value": true }, "machine_heated_bed": { "default_value": true },
"machine_heated_build_volume": { "default_value": true },
"machine_nozzle_heat_up_speed": { "default_value": 1.4 }, "machine_nozzle_heat_up_speed": { "default_value": 1.4 },
"machine_nozzle_cool_down_speed": { "default_value": 0.8 }, "machine_nozzle_cool_down_speed": { "default_value": 0.8 },
"machine_head_with_fans_polygon": "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 #: fdmprinter.def.json
msgctxt "xy_offset label" msgctxt "xy_offset label"
msgid "Horizontal Expansion" msgid "Horizontal Expansion"
msgstr "Vitesse dimpression horizontale" msgstr "Expansion horizontale"
#: fdmprinter.def.json #: fdmprinter.def.json
msgctxt "xy_offset description" msgctxt "xy_offset description"

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

@ -5,11 +5,14 @@ from unittest.mock import MagicMock
import pytest import pytest
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.Peripheral import Peripheral
test_validate_data_get_set = [ test_validate_data_get_set = [
{"attribute": "name", "value": "YAY"}, {"attribute": "name", "value": "YAY"},
{"attribute": "targetBedTemperature", "value": 192}, {"attribute": "targetBedTemperature", "value": 192},
{"attribute": "cameraUrl", "value": "YAY!"}
] ]
test_validate_data_get_update = [ test_validate_data_get_update = [
@ -22,6 +25,7 @@ test_validate_data_get_update = [
{"attribute": "targetBedTemperature", "value": 9001}, {"attribute": "targetBedTemperature", "value": 9001},
{"attribute": "activePrintJob", "value": PrintJobOutputModel(MagicMock())}, {"attribute": "activePrintJob", "value": PrintJobOutputModel(MagicMock())},
{"attribute": "state", "value": "BEEPBOOP"}, {"attribute": "state", "value": "BEEPBOOP"},
] ]
@ -79,3 +83,67 @@ def test_getAndUpdate(data):
getattr(model, "update" + attribute)(data["value"]) getattr(model, "update" + attribute)(data["value"])
# The signal should not fire again # The signal should not fire again
assert signal.emit.call_count == 1 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 from unittest.mock import MagicMock
import pytest 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 from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
test_validate_data_get_set = [ test_validate_data_get_set = [
@ -8,10 +14,15 @@ test_validate_data_get_set = [
{"attribute": "connectionState", "value": 1}, {"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) @pytest.mark.parametrize("data", test_validate_data_get_set)
def test_getAndSet(data): def test_getAndSet(data, printer_output_device):
model = PrinterOutputDevice("whatever") model = printer_output_device
# Convert the first letter into a capital # Convert the first letter into a capital
attribute = list(data["attribute"]) attribute = list(data["attribute"])
@ -35,3 +46,43 @@ def test_getAndSet(data):
getattr(model, "set" + attribute)(data["value"]) getattr(model, "set" + attribute)(data["value"])
# The signal should not fire again # The signal should not fire again
assert signal.emit.call_count == 1 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 datetime import datetime
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import requests import requests
from PyQt5.QtGui import QDesktopServices
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.AuthorizationService import AuthorizationService
@ -172,12 +173,12 @@ def test_storeAuthData(get_user_profile) -> None:
@patch.object(LocalAuthorizationServer, "stop") @patch.object(LocalAuthorizationServer, "stop")
@patch.object(LocalAuthorizationServer, "start") @patch.object(LocalAuthorizationServer, "start")
@patch.object(webbrowser, "open_new") @patch.object(QDesktopServices, "openUrl")
def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None: def test_localAuthServer(QDesktopServices_openUrl, start_auth_server, stop_auth_server) -> None:
preferences = Preferences() preferences = Preferences()
authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
authorization_service.startAuthorizationFlow() 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. # Ensure that the Authorization service tried to start the server.
assert start_auth_server.call_count == 1 assert start_auth_server.call_count == 1