Merge branch 'master' into fix_marlin_press_to_resume

This commit is contained in:
fieldOfView 2018-10-18 15:35:57 +02:00
commit f4c88aff0f
112 changed files with 1546 additions and 1282 deletions

View file

@ -1,15 +1,26 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtGui import QImage from PyQt5.QtGui import QImage
from PyQt5.QtQuick import QQuickImageProvider from PyQt5.QtQuick import QQuickImageProvider
from PyQt5.QtCore import QSize from PyQt5.QtCore import QSize
from UM.Application import Application from UM.Application import Application
## Creates screenshots of the current scene.
class CameraImageProvider(QQuickImageProvider): class CameraImageProvider(QQuickImageProvider):
def __init__(self): def __init__(self):
super().__init__(QQuickImageProvider.Image) super().__init__(QQuickImageProvider.Image)
## Request a new image. ## Request a new image.
#
# The image will be taken using the current camera position.
# Only the actual objects in the scene will get rendered. Not the build
# plate and such!
# \param id The ID for the image to create. This is the requested image
# source, with the "image:" scheme and provider identifier removed. It's
# a Qt thing, they'll provide this parameter.
# \param size The dimensions of the image to scale to.
def requestImage(self, id, size): def requestImage(self, id, size):
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
try: try:

View file

@ -4,7 +4,7 @@
import os import os
import sys import sys
import time import time
from typing import cast, TYPE_CHECKING from typing import cast, TYPE_CHECKING, Optional
import numpy import numpy
@ -109,6 +109,7 @@ from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisi
from cura.Settings.ContainerManager import ContainerManager from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
import cura.Settings.cura_empty_instance_containers import cura.Settings.cura_empty_instance_containers
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.ObjectsModel import ObjectsModel from cura.ObjectsModel import ObjectsModel
@ -176,6 +177,8 @@ class CuraApplication(QtApplication):
self._single_instance = None self._single_instance = None
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
self._cura_package_manager = None self._cura_package_manager = None
self._machine_action_manager = None self._machine_action_manager = None
@ -325,6 +328,8 @@ class CuraApplication(QtApplication):
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in # Adds custom property types, settings types, and extra operators (functions) that need to be registered in
# SettingDefinition and SettingFunction. # SettingDefinition and SettingFunction.
def __initializeSettingDefinitionsAndFunctions(self): def __initializeSettingDefinitionsAndFunctions(self):
self._cura_formula_functions = CuraFormulaFunctions(self)
# Need to do this before ContainerRegistry tries to load the machines # Need to do this before ContainerRegistry tries to load the machines
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
@ -345,10 +350,10 @@ class CuraApplication(QtApplication):
SettingDefinition.addSettingType("optional_extruder", None, str, None) SettingDefinition.addSettingType("optional_extruder", None, str, None)
SettingDefinition.addSettingType("[int]", None, str, None) SettingDefinition.addSettingType("[int]", None, str, None)
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues) SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue) SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue) SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", ExtruderManager.getDefaultExtruderPosition) SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
# Adds all resources and container related resources. # Adds all resources and container related resources.
def __addAllResourcesAndContainerResources(self) -> None: def __addAllResourcesAndContainerResources(self) -> None:
@ -709,10 +714,8 @@ class CuraApplication(QtApplication):
self._print_information = PrintInformation.PrintInformation(self) self._print_information = PrintInformation.PrintInformation(self)
self._cura_actions = CuraActions.CuraActions(self) self._cura_actions = CuraActions.CuraActions(self)
# Initialize setting visibility presets model # Initialize setting visibility presets model.
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self) self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
# Initialize Cura API # Initialize Cura API
self._cura_API.initialize() self._cura_API.initialize()
@ -815,6 +818,11 @@ class CuraApplication(QtApplication):
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel: def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
return self._setting_visibility_presets_model return self._setting_visibility_presets_model
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
if self._cura_formula_functions is None:
self._cura_formula_functions = CuraFormulaFunctions(self)
return self._cura_formula_functions
def getMachineErrorChecker(self, *args) -> MachineErrorChecker: def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
return self._machine_error_checker return self._machine_error_checker

View file

@ -1,135 +1,109 @@
# 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 Optional, List, Dict, Union from typing import Optional, List
import os
import urllib.parse
from configparser import ConfigParser
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Resources import Resources from UM.Resources import Resources
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Settings.SettingVisibilityPreset import SettingVisibilityPreset
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class SettingVisibilityPresetsModel(ListModel): class SettingVisibilityPresetsModel(QObject):
IdRole = Qt.UserRole + 1 onItemsChanged = pyqtSignal()
NameRole = Qt.UserRole + 2 activePresetChanged = pyqtSignal()
SettingsRole = Qt.UserRole + 3
def __init__(self, parent = None): def __init__(self, preferences, parent = None):
super().__init__(parent) super().__init__(parent)
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.SettingsRole, "settings")
self._items = [] # type: List[SettingVisibilityPreset]
self._populate() self._populate()
basic_item = self.items[1]
basic_visibile_settings = ";".join(basic_item["settings"])
self._preferences = Application.getInstance().getPreferences() basic_item = self.getVisibilityPresetById("basic")
basic_visibile_settings = ";".join(basic_item.settings)
self._preferences = preferences
# Preference to store which preset is currently selected # Preference to store which preset is currently selected
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic") self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
# Preference that stores the "custom" set so it can always be restored (even after a restart) # Preference that stores the "custom" set so it can always be restored (even after a restart)
self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings) self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
self._preferences.preferenceChanged.connect(self._onPreferencesChanged) self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset")) self._active_preset_item = self.getVisibilityPresetById(self._preferences.getValue("cura/active_setting_visibility_preset"))
# Initialize visible settings if it is not done yet # Initialize visible settings if it is not done yet
visible_settings = self._preferences.getValue("general/visible_settings") visible_settings = self._preferences.getValue("general/visible_settings")
if not visible_settings: if not visible_settings:
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"])) self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings))
else: else:
self._onPreferencesChanged("general/visible_settings") self._onPreferencesChanged("general/visible_settings")
self.activePresetChanged.emit() self.activePresetChanged.emit()
def _getItem(self, item_id: str) -> Optional[dict]: def getVisibilityPresetById(self, item_id: str) -> Optional[SettingVisibilityPreset]:
result = None result = None
for item in self.items: for item in self._items:
if item["id"] == item_id: if item.presetId == item_id:
result = item result = item
break break
return result return result
def _populate(self) -> None: def _populate(self) -> None:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
items = [] # type: List[Dict[str, Union[str, int, List[str]]]] items = [] # type: List[SettingVisibilityPreset]
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
items.append(custom_preset)
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset): for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
setting_visibility_preset = SettingVisibilityPreset()
try: try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) setting_visibility_preset.loadFromFile(file_path)
except MimeTypeNotFoundError:
Logger.log("e", "Could not determine mime type of file %s", file_path)
continue
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
if not os.path.isfile(file_path):
Logger.log("e", "[%s] is not a file", file_path)
continue
parser = ConfigParser(allow_no_value = True) # accept options without any value,
try:
parser.read([file_path])
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
continue
settings = [] # type: List[str]
for section in parser.sections():
if section == 'general':
continue
settings.append(section)
for option in parser[section].keys():
settings.append(option)
items.append({
"id": item_id,
"name": catalog.i18nc("@action:inmenu", parser["general"]["name"]),
"weight": parser["general"]["weight"],
"settings": settings,
})
except Exception: except Exception:
Logger.logException("e", "Failed to load setting preset %s", file_path) Logger.logException("e", "Failed to load setting preset %s", file_path)
items.sort(key = lambda k: (int(k["weight"]), k["id"])) # type: ignore items.append(setting_visibility_preset)
# Put "custom" at the top
items.insert(0, {"id": "custom", # Sort them on weight (and if that fails, use ID)
"name": "Custom selection", items.sort(key = lambda k: (int(k.weight), k.presetId))
"weight": -100,
"settings": []})
self.setItems(items) self.setItems(items)
@pyqtProperty("QVariantList", notify = onItemsChanged)
def items(self):
return self._items
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
if self._items != items:
self._items = items
self.onItemsChanged.emit()
@pyqtSlot(str) @pyqtSlot(str)
def setActivePreset(self, preset_id: str): def setActivePreset(self, preset_id: str) -> None:
if preset_id == self._active_preset_item["id"]: if preset_id == self._active_preset_item.presetId:
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id) Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
return return
preset_item = None preset_item = self.getVisibilityPresetById(preset_id)
for item in self.items:
if item["id"] == preset_id:
preset_item = item
break
if preset_item is None: if preset_item is None:
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id) Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
return return
need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom" need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom"
if need_to_save_to_custom: if need_to_save_to_custom:
# Save the current visibility settings to custom # Save the current visibility settings to custom
current_visibility_string = self._preferences.getValue("general/visible_settings") current_visibility_string = self._preferences.getValue("general/visible_settings")
if current_visibility_string: if current_visibility_string:
self._preferences.setValue("cura/custom_visible_settings", current_visibility_string) self._preferences.setValue("cura/custom_visible_settings", current_visibility_string)
new_visibility_string = ";".join(preset_item["settings"]) new_visibility_string = ";".join(preset_item.settings)
if preset_id == "custom": if preset_id == "custom":
# Get settings from the stored custom data # Get settings from the stored custom data
new_visibility_string = self._preferences.getValue("cura/custom_visible_settings") new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
@ -141,11 +115,9 @@ class SettingVisibilityPresetsModel(ListModel):
self._active_preset_item = preset_item self._active_preset_item = preset_item
self.activePresetChanged.emit() self.activePresetChanged.emit()
activePresetChanged = pyqtSignal()
@pyqtProperty(str, notify = activePresetChanged) @pyqtProperty(str, notify = activePresetChanged)
def activePreset(self) -> str: def activePreset(self) -> str:
return self._active_preset_item["id"] return self._active_preset_item.presetId
def _onPreferencesChanged(self, name: str) -> None: def _onPreferencesChanged(self, name: str) -> None:
if name != "general/visible_settings": if name != "general/visible_settings":
@ -158,25 +130,26 @@ class SettingVisibilityPresetsModel(ListModel):
visibility_set = set(visibility_string.split(";")) visibility_set = set(visibility_string.split(";"))
matching_preset_item = None matching_preset_item = None
for item in self.items: for item in self._items:
if item["id"] == "custom": if item.presetId == "custom":
continue continue
if set(item["settings"]) == visibility_set: if set(item.settings) == visibility_set:
matching_preset_item = item matching_preset_item = item
break break
item_to_set = self._active_preset_item item_to_set = self._active_preset_item
if matching_preset_item is None: if matching_preset_item is None:
# The new visibility setup is "custom" should be custom # The new visibility setup is "custom" should be custom
if self._active_preset_item["id"] == "custom": if self._active_preset_item.presetId == "custom":
# We are already in custom, just save the settings # We are already in custom, just save the settings
self._preferences.setValue("cura/custom_visible_settings", visibility_string) self._preferences.setValue("cura/custom_visible_settings", visibility_string)
else: else:
item_to_set = self.items[0] # 0 is custom # We need to move to custom preset.
item_to_set = self.getVisibilityPresetById("custom")
else: else:
item_to_set = matching_preset_item item_to_set = matching_preset_item
if self._active_preset_item is None or self._active_preset_item["id"] != item_to_set["id"]: if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
self._active_preset_item = item_to_set self._active_preset_item = item_to_set
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item["id"]) self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
self.activePresetChanged.emit() self.activePresetChanged.emit()

View file

@ -0,0 +1,78 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
from enum import IntEnum
from threading import Thread
from typing import Union
MYPY = False
if MYPY:
from cura.PrinterOutputDevice import PrinterOutputDevice
class FirmwareUpdater(QObject):
firmwareProgressChanged = pyqtSignal()
firmwareUpdateStateChanged = pyqtSignal()
def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__()
self._output_device = output_device
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_file = ""
self._firmware_progress = 0
self._firmware_update_state = FirmwareUpdateState.idle
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
# the file path could be url-encoded.
if firmware_file.startswith("file://"):
self._firmware_file = QUrl(firmware_file).toLocalFile()
else:
self._firmware_file = firmware_file
self._setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()
def _updateFirmware(self) -> None:
raise NotImplementedError("_updateFirmware needs to be implemented")
## Cleanup after a succesful update
def _cleanupAfterUpdate(self) -> None:
# Clean up for next attempt.
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_file = ""
self._onFirmwareProgress(100)
self._setFirmwareUpdateState(FirmwareUpdateState.completed)
@pyqtProperty(int, notify = firmwareProgressChanged)
def firmwareProgress(self) -> int:
return self._firmware_progress
@pyqtProperty(int, notify=firmwareUpdateStateChanged)
def firmwareUpdateState(self) -> "FirmwareUpdateState":
return self._firmware_update_state
def _setFirmwareUpdateState(self, state: "FirmwareUpdateState") -> None:
if self._firmware_update_state != state:
self._firmware_update_state = state
self.firmwareUpdateStateChanged.emit()
# Callback function for firmware update progress.
def _onFirmwareProgress(self, progress: int, max_progress: int = 100) -> None:
self._firmware_progress = int(progress * 100 / max_progress) # Convert to scale of 0-100
self.firmwareProgressChanged.emit()
class FirmwareUpdateState(IntEnum):
idle = 0
updating = 1
completed = 2
unknown_error = 3
communication_error = 4
io_error = 5
firmware_not_found_error = 6

View file

@ -1,7 +1,7 @@
# 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 typing import TYPE_CHECKING, Set, Union, Optional
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
@ -9,27 +9,28 @@ from PyQt5.QtCore import QTimer
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
class GenericOutputController(PrinterOutputController): class GenericOutputController(PrinterOutputController):
def __init__(self, output_device): def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__(output_device) super().__init__(output_device)
self._preheat_bed_timer = QTimer() self._preheat_bed_timer = QTimer()
self._preheat_bed_timer.setSingleShot(True) self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished) self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
self._preheat_printer = None self._preheat_printer = None # type: Optional[PrinterOutputModel]
self._preheat_hotends_timer = QTimer() self._preheat_hotends_timer = QTimer()
self._preheat_hotends_timer.setSingleShot(True) self._preheat_hotends_timer.setSingleShot(True)
self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished) self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished)
self._preheat_hotends = set() self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
self._output_device.printersChanged.connect(self._onPrintersChanged) self._output_device.printersChanged.connect(self._onPrintersChanged)
self._active_printer = None self._active_printer = None # type: Optional[PrinterOutputModel]
def _onPrintersChanged(self): def _onPrintersChanged(self) -> None:
if self._active_printer: if self._active_printer:
self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged) self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged)
self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged) self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged)
@ -43,32 +44,33 @@ class GenericOutputController(PrinterOutputController):
for extruder in self._active_printer.extruders: for extruder in self._active_printer.extruders:
extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged) extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged)
def _onPrinterStateChanged(self): def _onPrinterStateChanged(self) -> None:
if self._active_printer.state != "idle": if self._active_printer and self._active_printer.state != "idle":
if self._preheat_bed_timer.isActive(): if self._preheat_bed_timer.isActive():
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()
if self._preheat_printer:
self._preheat_printer.updateIsPreheating(False) self._preheat_printer.updateIsPreheating(False)
if self._preheat_hotends_timer.isActive(): if self._preheat_hotends_timer.isActive():
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
self._preheat_hotends = set() self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
self._output_device.sendCommand("G91") self._output_device.sendCommand("G91")
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
self._output_device.sendCommand("G90") self._output_device.sendCommand("G90")
def homeHead(self, printer): def homeHead(self, printer: "PrinterOutputModel") -> None:
self._output_device.sendCommand("G28 X Y") self._output_device.sendCommand("G28 X Y")
def homeBed(self, printer): def homeBed(self, printer: "PrinterOutputModel") -> None:
self._output_device.sendCommand("G28 Z") self._output_device.sendCommand("G28 Z")
def sendRawCommand(self, printer: "PrinterOutputModel", command: str): def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands. self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands.
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
if state == "pause": if state == "pause":
self._output_device.pausePrint() self._output_device.pausePrint()
job.updateState("paused") job.updateState("paused")
@ -79,15 +81,15 @@ class GenericOutputController(PrinterOutputController):
self._output_device.cancelPrint() self._output_device.cancelPrint()
pass pass
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
self._output_device.sendCommand("M140 S%s" % temperature) self._output_device.sendCommand("M140 S%s" % temperature)
def _onTargetBedTemperatureChanged(self): def _onTargetBedTemperatureChanged(self) -> None:
if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0: if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()
self._preheat_printer.updateIsPreheating(False) self._preheat_printer.updateIsPreheating(False)
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
try: try:
temperature = round(temperature) # The API doesn't allow floating point. temperature = round(temperature) # The API doesn't allow floating point.
duration = round(duration) duration = round(duration)
@ -100,21 +102,25 @@ class GenericOutputController(PrinterOutputController):
self._preheat_printer = printer self._preheat_printer = printer
printer.updateIsPreheating(True) printer.updateIsPreheating(True)
def cancelPreheatBed(self, printer: "PrinterOutputModel"): def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
self.setTargetBedTemperature(printer, temperature=0) self.setTargetBedTemperature(printer, temperature=0)
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()
printer.updateIsPreheating(False) printer.updateIsPreheating(False)
def _onPreheatBedTimerFinished(self): def _onPreheatBedTimerFinished(self) -> None:
if not self._preheat_printer:
return
self.setTargetBedTemperature(self._preheat_printer, 0) self.setTargetBedTemperature(self._preheat_printer, 0)
self._preheat_printer.updateIsPreheating(False) self._preheat_printer.updateIsPreheating(False)
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int): def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
self._output_device.sendCommand("M104 S%s T%s" % (temperature, position)) self._output_device.sendCommand("M104 S%s T%s" % (temperature, position))
def _onTargetHotendTemperatureChanged(self): def _onTargetHotendTemperatureChanged(self) -> None:
if not self._preheat_hotends_timer.isActive(): if not self._preheat_hotends_timer.isActive():
return return
if not self._active_printer:
return
for extruder in self._active_printer.extruders: for extruder in self._active_printer.extruders:
if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0: if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0:
@ -123,7 +129,7 @@ class GenericOutputController(PrinterOutputController):
if not self._preheat_hotends: if not self._preheat_hotends:
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
position = extruder.getPosition() position = extruder.getPosition()
number_of_extruders = len(extruder.getPrinter().extruders) number_of_extruders = len(extruder.getPrinter().extruders)
if position >= number_of_extruders: if position >= number_of_extruders:
@ -141,7 +147,7 @@ class GenericOutputController(PrinterOutputController):
self._preheat_hotends.add(extruder) self._preheat_hotends.add(extruder)
extruder.updateIsPreheating(True) extruder.updateIsPreheating(True)
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0) self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0)
if extruder in self._preheat_hotends: if extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
@ -149,21 +155,22 @@ class GenericOutputController(PrinterOutputController):
if not self._preheat_hotends and self._preheat_hotends_timer.isActive(): if not self._preheat_hotends and self._preheat_hotends_timer.isActive():
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
def _onPreheatHotendsTimerFinished(self): def _onPreheatHotendsTimerFinished(self) -> None:
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0) self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
self._preheat_hotends = set() self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
# Cancel any ongoing preheating timers, without setting back the temperature to 0 # Cancel any ongoing preheating timers, without setting back the temperature to 0
# This can be used eg at the start of a print # This can be used eg at the start of a print
def stopPreheatTimers(self): def stopPreheatTimers(self) -> None:
if self._preheat_hotends_timer.isActive(): if self._preheat_hotends_timer.isActive():
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
self._preheat_hotends = set() self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
if self._preheat_bed_timer.isActive(): if self._preheat_bed_timer.isActive():
if self._preheat_printer:
self._preheat_printer.updateIsPreheating(False) self._preheat_printer.updateIsPreheating(False)
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()

View file

@ -130,9 +130,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
# sleep. # sleep.
if time_since_last_response > self._recreate_network_manager_time: if time_since_last_response > self._recreate_network_manager_time:
if self._last_manager_create_time is None: if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
self._createNetworkManager()
elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
self._createNetworkManager() self._createNetworkManager()
assert(self._manager is not None) assert(self._manager is not None)
elif self._connection_state == ConnectionState.closed: elif self._connection_state == ConnectionState.closed:

View file

@ -91,7 +91,7 @@ class PrintJobOutputModel(QObject):
def assignedPrinter(self): def assignedPrinter(self):
return self._assigned_printer return self._assigned_printer
def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"): def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
if self._assigned_printer != assigned_printer: if self._assigned_printer != assigned_printer:
old_printer = self._assigned_printer old_printer = self._assigned_printer
self._assigned_printer = assigned_printer self._assigned_printer = assigned_printer

View file

@ -1,57 +1,68 @@
# Copyright (c) 2017 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 UM.Logger import Logger from UM.Logger import Logger
from UM.Signal import Signal
from typing import Union
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
class PrinterOutputController: class PrinterOutputController:
def __init__(self, output_device): def __init__(self, output_device: "PrinterOutputDevice") -> None:
self.can_pause = True self.can_pause = True
self.can_abort = True self.can_abort = True
self.can_pre_heat_bed = True self.can_pre_heat_bed = True
self.can_pre_heat_hotends = True self.can_pre_heat_hotends = True
self.can_send_raw_gcode = True self.can_send_raw_gcode = True
self.can_control_manually = True self.can_control_manually = True
self.can_update_firmware = False
self._output_device = output_device self._output_device = output_device
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOutputModel", temperature: int): def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
Logger.log("w", "Set target hotend temperature not implemented in controller") Logger.log("w", "Set target hotend temperature not implemented in controller")
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
Logger.log("w", "Set target bed temperature not implemented in controller") Logger.log("w", "Set target bed temperature not implemented in controller")
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
Logger.log("w", "Set job state not implemented in controller") Logger.log("w", "Set job state not implemented in controller")
def cancelPreheatBed(self, printer: "PrinterOutputModel"): def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Cancel preheat bed not implemented in controller") Logger.log("w", "Cancel preheat bed not implemented in controller")
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
Logger.log("w", "Preheat bed not implemented in controller") Logger.log("w", "Preheat bed not implemented in controller")
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
Logger.log("w", "Cancel preheat hotend not implemented in controller") Logger.log("w", "Cancel preheat hotend not implemented in controller")
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
Logger.log("w", "Preheat hotend not implemented in controller") Logger.log("w", "Preheat hotend not implemented in controller")
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed): def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
Logger.log("w", "Set head position not implemented in controller") Logger.log("w", "Set head position not implemented in controller")
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
Logger.log("w", "Move head not implemented in controller") Logger.log("w", "Move head not implemented in controller")
def homeBed(self, printer: "PrinterOutputModel"): def homeBed(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Home bed not implemented in controller") Logger.log("w", "Home bed not implemented in controller")
def homeHead(self, printer: "PrinterOutputModel"): def homeHead(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Home head not implemented in controller") Logger.log("w", "Home head not implemented in controller")
def sendRawCommand(self, printer: "PrinterOutputModel", command: str): def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
Logger.log("w", "Custom command not implemented in controller") Logger.log("w", "Custom command not implemented in controller")
canUpdateFirmwareChanged = Signal()
def setCanUpdateFirmware(self, can_update_firmware: bool) -> None:
if can_update_firmware != self.can_update_firmware:
self.can_update_firmware = can_update_firmware
self.canUpdateFirmwareChanged.emit()

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
from typing import Optional from typing import List, Dict, Optional
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
@ -11,6 +11,7 @@ MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.NetworkCamera import NetworkCamera
class PrinterOutputModel(QObject): class PrinterOutputModel(QObject):
@ -26,6 +27,7 @@ class PrinterOutputModel(QObject):
buildplateChanged = pyqtSignal() buildplateChanged = pyqtSignal()
cameraChanged = pyqtSignal() cameraChanged = pyqtSignal()
configurationChanged = pyqtSignal() configurationChanged = pyqtSignal()
canUpdateFirmwareChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None: def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
super().__init__(parent) super().__init__(parent)
@ -34,6 +36,7 @@ class PrinterOutputModel(QObject):
self._name = "" self._name = ""
self._key = "" # Unique identifier self._key = "" # Unique identifier
self._controller = output_controller self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0) self._head_position = Vector(0, 0, 0)
@ -42,7 +45,7 @@ class PrinterOutputModel(QObject):
self._printer_state = "unknown" self._printer_state = "unknown"
self._is_preheating = False self._is_preheating = False
self._printer_type = "" self._printer_type = ""
self._buildplate_name = None self._buildplate_name = ""
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
self._extruders] self._extruders]
@ -50,32 +53,32 @@ class PrinterOutputModel(QObject):
self._camera = None self._camera = None
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def firmwareVersion(self): def firmwareVersion(self) -> str:
return self._firmware_version return self._firmware_version
def setCamera(self, camera): def setCamera(self, camera: Optional["NetworkCamera"]) -> None:
if self._camera is not camera: if self._camera is not camera:
self._camera = camera self._camera = camera
self.cameraChanged.emit() self.cameraChanged.emit()
def updateIsPreheating(self, pre_heating): def updateIsPreheating(self, pre_heating: bool) -> None:
if self._is_preheating != pre_heating: if self._is_preheating != pre_heating:
self._is_preheating = pre_heating self._is_preheating = pre_heating
self.isPreheatingChanged.emit() self.isPreheatingChanged.emit()
@pyqtProperty(bool, notify=isPreheatingChanged) @pyqtProperty(bool, notify=isPreheatingChanged)
def isPreheating(self): def isPreheating(self) -> bool:
return self._is_preheating return self._is_preheating
@pyqtProperty(QObject, notify=cameraChanged) @pyqtProperty(QObject, notify=cameraChanged)
def camera(self): def camera(self) -> Optional["NetworkCamera"]:
return self._camera return self._camera
@pyqtProperty(str, notify = printerTypeChanged) @pyqtProperty(str, notify = printerTypeChanged)
def type(self): def type(self) -> str:
return self._printer_type return self._printer_type
def updateType(self, printer_type): 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._printer_configuration.printerType = self._printer_type
@ -83,10 +86,10 @@ class PrinterOutputModel(QObject):
self.configurationChanged.emit() self.configurationChanged.emit()
@pyqtProperty(str, notify = buildplateChanged) @pyqtProperty(str, notify = buildplateChanged)
def buildplate(self): def buildplate(self) -> str:
return self._buildplate_name return self._buildplate_name
def updateBuildplateName(self, buildplate_name): def updateBuildplateName(self, buildplate_name: str) -> None:
if self._buildplate_name != buildplate_name: if self._buildplate_name != buildplate_name:
self._buildplate_name = buildplate_name self._buildplate_name = buildplate_name
self._printer_configuration.buildplateConfiguration = self._buildplate_name self._printer_configuration.buildplateConfiguration = self._buildplate_name
@ -94,66 +97,66 @@ class PrinterOutputModel(QObject):
self.configurationChanged.emit() self.configurationChanged.emit()
@pyqtProperty(str, notify=keyChanged) @pyqtProperty(str, notify=keyChanged)
def key(self): def key(self) -> str:
return self._key return self._key
def updateKey(self, key: str): def updateKey(self, key: str) -> None:
if self._key != key: if self._key != key:
self._key = key self._key = key
self.keyChanged.emit() self.keyChanged.emit()
@pyqtSlot() @pyqtSlot()
def homeHead(self): def homeHead(self) -> None:
self._controller.homeHead(self) self._controller.homeHead(self)
@pyqtSlot() @pyqtSlot()
def homeBed(self): def homeBed(self) -> None:
self._controller.homeBed(self) self._controller.homeBed(self)
@pyqtSlot(str) @pyqtSlot(str)
def sendRawCommand(self, command: str): def sendRawCommand(self, command: str) -> None:
self._controller.sendRawCommand(self, command) self._controller.sendRawCommand(self, command)
@pyqtProperty("QVariantList", constant = True) @pyqtProperty("QVariantList", constant = True)
def extruders(self): def extruders(self) -> List["ExtruderOutputModel"]:
return self._extruders return self._extruders
@pyqtProperty(QVariant, notify = headPositionChanged) @pyqtProperty(QVariant, notify = headPositionChanged)
def headPosition(self): def headPosition(self) -> Dict[str, float]:
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z} return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
def updateHeadPosition(self, x, y, z): def updateHeadPosition(self, x: float, y: float, z: float) -> None:
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z: if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
self._head_position = Vector(x, y, z) self._head_position = Vector(x, y, z)
self.headPositionChanged.emit() self.headPositionChanged.emit()
@pyqtProperty(float, float, float) @pyqtProperty(float, float, float)
@pyqtProperty(float, float, float, float) @pyqtProperty(float, float, float, float)
def setHeadPosition(self, x, y, z, speed = 3000): def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
self.updateHeadPosition(x, y, z) self.updateHeadPosition(x, y, z)
self._controller.setHeadPosition(self, x, y, z, speed) self._controller.setHeadPosition(self, x, y, z, speed)
@pyqtProperty(float) @pyqtProperty(float)
@pyqtProperty(float, float) @pyqtProperty(float, float)
def setHeadX(self, x, speed = 3000): def setHeadX(self, x: float, speed: float = 3000) -> None:
self.updateHeadPosition(x, self._head_position.y, self._head_position.z) self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed) self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
@pyqtProperty(float) @pyqtProperty(float)
@pyqtProperty(float, float) @pyqtProperty(float, float)
def setHeadY(self, y, speed = 3000): def setHeadY(self, y: float, speed: float = 3000) -> None:
self.updateHeadPosition(self._head_position.x, y, self._head_position.z) self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed) self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
@pyqtProperty(float) @pyqtProperty(float)
@pyqtProperty(float, float) @pyqtProperty(float, float)
def setHeadZ(self, z, speed = 3000): def setHeadZ(self, z: float, speed:float = 3000) -> None:
self.updateHeadPosition(self._head_position.x, self._head_position.y, z) self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed) self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
@pyqtSlot(float, float, float) @pyqtSlot(float, float, float)
@pyqtSlot(float, float, float, float) @pyqtSlot(float, float, float, float)
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000): def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
self._controller.moveHead(self, x, y, z, speed) self._controller.moveHead(self, x, y, z, speed)
## Pre-heats the heated bed of the printer. ## Pre-heats the heated bed of the printer.
@ -162,47 +165,47 @@ class PrinterOutputModel(QObject):
# Celsius. # Celsius.
# \param duration How long the bed should stay warm, in seconds. # \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float) @pyqtSlot(float, float)
def preheatBed(self, temperature, duration): def preheatBed(self, temperature: float, duration: float) -> None:
self._controller.preheatBed(self, temperature, duration) self._controller.preheatBed(self, temperature, duration)
@pyqtSlot() @pyqtSlot()
def cancelPreheatBed(self): def cancelPreheatBed(self) -> None:
self._controller.cancelPreheatBed(self) self._controller.cancelPreheatBed(self)
def getController(self): def getController(self) -> "PrinterOutputController":
return self._controller return self._controller
@pyqtProperty(str, notify = nameChanged) @pyqtProperty(str, notify = nameChanged)
def name(self): def name(self) -> str:
return self._name return self._name
def setName(self, name): def setName(self, name: str) -> None:
self._setName(name) self._setName(name)
self.updateName(name) self.updateName(name)
def updateName(self, name): def updateName(self, name: str) -> None:
if self._name != name: if self._name != name:
self._name = name self._name = name
self.nameChanged.emit() self.nameChanged.emit()
## Update the bed temperature. This only changes it locally. ## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature): def updateBedTemperature(self, temperature: int) -> None:
if self._bed_temperature != temperature: if self._bed_temperature != temperature:
self._bed_temperature = temperature self._bed_temperature = temperature
self.bedTemperatureChanged.emit() self.bedTemperatureChanged.emit()
def updateTargetBedTemperature(self, temperature): def updateTargetBedTemperature(self, temperature: int) -> None:
if self._target_bed_temperature != temperature: if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit() self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote. ## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(int) @pyqtSlot(int)
def setTargetBedTemperature(self, temperature): def setTargetBedTemperature(self, temperature: int) -> None:
self._controller.setTargetBedTemperature(self, temperature) self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature) self.updateTargetBedTemperature(temperature)
def updateActivePrintJob(self, print_job): def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
if self._active_print_job != print_job: if self._active_print_job != print_job:
old_print_job = self._active_print_job old_print_job = self._active_print_job
@ -214,72 +217,83 @@ class PrinterOutputModel(QObject):
old_print_job.updateAssignedPrinter(None) old_print_job.updateAssignedPrinter(None)
self.activePrintJobChanged.emit() self.activePrintJobChanged.emit()
def updateState(self, printer_state): def updateState(self, printer_state: str) -> None:
if self._printer_state != printer_state: if self._printer_state != printer_state:
self._printer_state = printer_state self._printer_state = printer_state
self.stateChanged.emit() self.stateChanged.emit()
@pyqtProperty(QObject, notify = activePrintJobChanged) @pyqtProperty(QObject, notify = activePrintJobChanged)
def activePrintJob(self): def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
return self._active_print_job return self._active_print_job
@pyqtProperty(str, notify=stateChanged) @pyqtProperty(str, notify=stateChanged)
def state(self): def state(self) -> str:
return self._printer_state return self._printer_state
@pyqtProperty(int, notify=bedTemperatureChanged) @pyqtProperty(int, notify=bedTemperatureChanged)
def bedTemperature(self): def bedTemperature(self) -> int:
return self._bed_temperature return self._bed_temperature
@pyqtProperty(int, notify=targetBedTemperatureChanged) @pyqtProperty(int, notify=targetBedTemperatureChanged)
def targetBedTemperature(self): def targetBedTemperature(self) -> int:
return self._target_bed_temperature return self._target_bed_temperature
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPreHeatBed(self): def canPreHeatBed(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pre_heat_bed return self._controller.can_pre_heat_bed
return False return False
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPreHeatHotends(self): def canPreHeatHotends(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pre_heat_hotends return self._controller.can_pre_heat_hotends
return False return False
# Does the printer support sending raw G-code at all # Does the printer support sending raw G-code at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canSendRawGcode(self): def canSendRawGcode(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_send_raw_gcode return self._controller.can_send_raw_gcode
return False return False
# Does the printer support pause at all # Does the printer support pause at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPause(self): def canPause(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pause return self._controller.can_pause
return False return False
# Does the printer support abort at all # Does the printer support abort at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canAbort(self): def canAbort(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_abort return self._controller.can_abort
return False return False
# Does the printer support manual control at all # Does the printer support manual control at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canControlManually(self): def canControlManually(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_control_manually return self._controller.can_control_manually
return False return False
# Does the printer support upgrading firmware
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
def canUpdateFirmware(self) -> bool:
if self._controller:
return self._controller.can_update_firmware
return False
# Stub to connect UM.Signal to pyqtSignal
def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.canUpdateFirmwareChanged.emit()
# Returns the configuration (material, variant and buildplate) of the current printer # Returns the configuration (material, variant and buildplate) of the current printer
@pyqtProperty(QObject, notify = configurationChanged) @pyqtProperty(QObject, notify = configurationChanged)
def printerConfiguration(self): def printerConfiguration(self) -> Optional[ConfigurationModel]:
if self._printer_configuration.isValid(): if self._printer_configuration.isValid():
return self._printer_configuration return self._printer_configuration
return None return None

View file

@ -4,22 +4,24 @@
from UM.Decorators import deprecated from UM.Decorators import deprecated
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.Logger import Logger from UM.Logger import Logger
from UM.FileHandler.FileHandler import FileHandler #For typing.
from UM.Scene.SceneNode import SceneNode #For typing.
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Qt.QtApplication import QtApplication from UM.Qt.QtApplication import QtApplication
from UM.FlameProfiler import pyqtSlot
from enum import IntEnum # For the connection state tracking. from enum import IntEnum # For the connection state tracking.
from typing import Callable, List, Optional from typing import Callable, List, Optional, Union
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
from UM.FileHandler.FileHandler import FileHandler
from UM.Scene.SceneNode import SceneNode
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -83,6 +85,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._connection_state = ConnectionState.closed #type: ConnectionState self._connection_state = ConnectionState.closed #type: ConnectionState
self._firmware_updater = None #type: Optional[FirmwareUpdater]
self._firmware_name = None #type: Optional[str] self._firmware_name = None #type: Optional[str]
self._address = "" #type: str self._address = "" #type: str
self._connection_text = "" #type: str self._connection_text = "" #type: str
@ -128,7 +131,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
return None return None
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
@pyqtProperty(QObject, notify = printersChanged) @pyqtProperty(QObject, notify = printersChanged)
@ -226,3 +229,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
# This name can be used to define device type # This name can be used to define device type
def getFirmwareName(self) -> Optional[str]: def getFirmwareName(self) -> Optional[str]:
return self._firmware_name return self._firmware_name
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
return self._firmware_updater
@pyqtSlot(str)
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
if not self._firmware_updater:
return
self._firmware_updater.updateFirmware(firmware_file)

View file

@ -187,11 +187,11 @@ class CuraContainerRegistry(ContainerRegistry):
try: try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
except NoProfileException: except NoProfileException:
return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "No custom profile to import in file <filename>{0}</filename>", file_name)} return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
except Exception as e: except Exception as e:
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e))
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))} return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>:", file_name) + "\n<message>" + str(e) + "</message>"}
if profile_or_list: if profile_or_list:
# Ensure it is always a list of profiles # Ensure it is always a list of profiles
@ -215,7 +215,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not global_profile: if not global_profile:
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
return { "status": "error", return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)} "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)}
profile_definition = global_profile.getMetaDataEntry("definition") profile_definition = global_profile.getMetaDataEntry("definition")
# Make sure we have a profile_definition in the file: # Make sure we have a profile_definition in the file:
@ -225,7 +225,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not machine_definition: if not machine_definition:
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
return {"status": "error", return {"status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name) "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
} }
machine_definition = machine_definition[0] machine_definition = machine_definition[0]
@ -238,7 +238,7 @@ class CuraContainerRegistry(ContainerRegistry):
if profile_definition != expected_machine_definition: if profile_definition != expected_machine_definition:
Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition) Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition)
return { "status": "error", return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)} "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
# Fix the global quality profile's definition field in case it's not correct # Fix the global quality profile's definition field in case it's not correct
global_profile.setMetaDataEntry("definition", expected_machine_definition) global_profile.setMetaDataEntry("definition", expected_machine_definition)
@ -269,8 +269,7 @@ class CuraContainerRegistry(ContainerRegistry):
if idx == 0: if idx == 0:
# move all per-extruder settings to the first extruder's quality_changes # move all per-extruder settings to the first extruder's quality_changes
for qc_setting_key in global_profile.getAllKeys(): for qc_setting_key in global_profile.getAllKeys():
settable_per_extruder = global_stack.getProperty(qc_setting_key, settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
"settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
setting_value = global_profile.getProperty(qc_setting_key, "value") setting_value = global_profile.getProperty(qc_setting_key, "value")
@ -310,8 +309,8 @@ class CuraContainerRegistry(ContainerRegistry):
if result is not None: if result is not None:
return {"status": "error", "message": catalog.i18nc( return {"status": "error", "message": catalog.i18nc(
"@info:status Don't translate the XML tags <filename> or <message>!", "@info:status Don't translate the XML tags <filename> or <message>!",
"Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", "Failed to import profile from <filename>{0}</filename>:",
file_name, result)} file_name) + " <message>" + result + "</message>"}
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}

View file

@ -0,0 +1,130 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, List, Optional, TYPE_CHECKING
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from UM.Settings.SettingFunction import SettingFunction
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Settings.CuraContainerStack import CuraContainerStack
#
# This class contains all Cura-related custom functions that can be used in formulas. Some functions requires
# information such as the currently active machine, so this is made into a class instead of standalone functions.
#
class CuraFormulaFunctions:
def __init__(self, application: "CuraApplication") -> None:
self._application = application
# ================
# Custom Functions
# ================
# Gets the default extruder position of the currently active machine.
def getDefaultExtruderPosition(self) -> str:
machine_manager = self._application.getMachineManager()
return machine_manager.defaultExtruderPosition
# Gets the given setting key from the given extruder position.
def getValueInExtruder(self, extruder_position: int, property_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
if extruder_position == -1:
extruder_position = int(machine_manager.defaultExtruderPosition)
global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)]
value = extruder_stack.getRawProperty(property_key, "value", context = context)
if isinstance(value, SettingFunction):
value = value(extruder_stack, context = context)
return value
# Gets all extruder values as a list for the given property.
def getValuesInAllExtruders(self, property_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> List[Any]:
machine_manager = self._application.getMachineManager()
extruder_manager = self._application.getExtruderManager()
global_stack = machine_manager.activeMachine
result = []
for extruder in extruder_manager.getActiveExtruderStacks():
if not extruder.isEnabled:
continue
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue
value = extruder.getRawProperty(property_key, "value", context = context)
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
result.append(value)
if not result:
result.append(global_stack.getProperty(property_key, "value", context = context))
return result
# Get the resolve value or value for a given key.
def getResolveOrValue(self, property_key: str, context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
resolved_value = global_stack.getProperty(property_key, "value", context = context)
return resolved_value
# Gets the default setting value from given extruder position. The default value is what excludes the values in
# the user_changes container.
def getDefaultValueInExtruder(self, extruder_position: int, property_key: str) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)]
context = self.createContextForDefaultValueEvaluation(extruder_stack)
return self.getValueInExtruder(extruder_position, property_key, context = context)
# Gets all default setting values as a list from all extruders of the currently active machine.
# The default values are those excluding the values in the user_changes container.
def getDefaultValuesInAllExtruders(self, property_key: str) -> List[Any]:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getValuesInAllExtruders(property_key, context = context)
# Gets the resolve value or value for a given key without looking the first container (user container).
def getDefaultResolveOrValue(self, property_key: str) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getResolveOrValue(property_key, context = context)
# Creates a context for evaluating default values (skip the user_changes container).
def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext":
context = PropertyEvaluationContext(source_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": self.getDefaultValueInExtruder,
"extruderValues": self.getDefaultValuesInAllExtruders,
"resolveOrValue": self.getDefaultResolveOrValue,
}
return context

View file

@ -12,9 +12,7 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
@ -69,16 +67,6 @@ class ExtruderManager(QObject):
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
return None return None
## Return extruder count according to extruder trains.
@pyqtProperty(int, notify = extrudersChanged)
def extruderCount(self) -> int:
if not self._application.getGlobalContainerStack():
return 0 # No active machine, so no extruders.
try:
return len(self._extruder_trains[self._application.getGlobalContainerStack().getId()])
except KeyError:
return 0
## Gets a dict with the extruder stack ids with the extruder number as the key. ## Gets a dict with the extruder stack ids with the extruder number as the key.
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruderIds(self) -> Dict[str, str]: def extruderIds(self) -> Dict[str, str]:
@ -386,85 +374,7 @@ class ExtruderManager(QObject):
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0] extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
extruder_stack_0.definition = extruder_definition extruder_stack_0.definition = extruder_definition
## Get all extruder values for a certain setting. extruder_stack_0.setNextStack(global_stack)
#
# This is exposed to SettingFunction so it can be used in value functions.
#
# \param key The key of the setting to retrieve values for.
#
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value.
@staticmethod
def getExtruderValues(key: str) -> List[Any]:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
result = []
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
if not extruder.isEnabled:
continue
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value"):
continue
value = extruder.getRawProperty(key, "value")
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder)
result.append(value)
if not result:
result.append(global_stack.getProperty(key, "value"))
return result
## Get all extruder values for a certain setting. This function will skip the user settings container.
#
# This is exposed to SettingFunction so it can be used in value functions.
#
# \param key The key of the setting to retrieve values for.
#
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value.
@staticmethod
def getDefaultExtruderValues(key: str) -> List[Any]:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
result = []
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue
value = extruder.getRawProperty(key, "value", context = context)
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
result.append(value)
if not result:
result.append(global_stack.getProperty(key, "value", context = context))
return result
## Return the default extruder position from the machine manager
@staticmethod
def getDefaultExtruderPosition() -> str:
return cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition
## Get all extruder values for a certain setting. ## Get all extruder values for a certain setting.
# #
@ -474,62 +384,8 @@ class ExtruderManager(QObject):
# #
# \return String representing the extruder values # \return String representing the extruder values
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
def getInstanceExtruderValues(self, key) -> List: def getInstanceExtruderValues(self, key: str) -> List:
return ExtruderManager.getExtruderValues(key) return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key)
## Get the value for a setting from a specific extruder.
#
# This is exposed to SettingFunction to use in value functions.
#
# \param extruder_index The index of the extruder to get the value from.
# \param key The key of the setting to get the value of.
#
# \return The value of the setting for the specified extruder or for the
# global stack if not found.
@staticmethod
def getExtruderValue(extruder_index: int, key: str) -> Any:
if extruder_index == -1:
extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if extruder:
value = extruder.getRawProperty(key, "value")
if isinstance(value, SettingFunction):
value = value(extruder)
else:
# Just a value from global.
value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value")
return value
## Get the default value from the given extruder. This function will skip the user settings container.
#
# This is exposed to SettingFunction to use in value functions.
#
# \param extruder_index The index of the extruder to get the value from.
# \param key The key of the setting to get the value of.
#
# \return The value of the setting for the specified extruder or for the
# global stack if not found.
@staticmethod
def getDefaultExtruderValue(extruder_index: int, key: str) -> Any:
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
context = PropertyEvaluationContext(extruder)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
if extruder:
value = extruder.getRawProperty(key, "value", context = context)
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
else: # Just a value from global.
value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value", context = context)
return value
## Get the resolve value or value for a given key ## Get the resolve value or value for a given key
# #
@ -545,28 +401,6 @@ class ExtruderManager(QObject):
return resolved_value return resolved_value
## Get the resolve value or value for a given key without looking the first container (user container)
#
# This is the effective value for a given key, it is used for values in the global stack.
# This is exposed to SettingFunction to use in value functions.
# \param key The key of the setting to get the value of.
#
# \return The effective value
@staticmethod
def getDefaultResolveOrValue(key: str) -> Any:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
resolved_value = global_stack.getProperty(key, "value", context = context)
return resolved_value
__instance = None # type: ExtruderManager __instance = None # type: ExtruderManager
@classmethod @classmethod

View file

@ -4,7 +4,7 @@
from collections import defaultdict from collections import defaultdict
import threading import threading
from typing import Any, Dict, Optional, Set, TYPE_CHECKING from typing import Any, Dict, Optional, Set, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty from PyQt5.QtCore import pyqtProperty, pyqtSlot
from UM.Decorators import override from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
@ -13,6 +13,8 @@ from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger from UM.Logger import Logger
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Util import parseBool from UM.Util import parseBool
import cura.CuraApplication import cura.CuraApplication
@ -200,6 +202,31 @@ class GlobalStack(CuraContainerStack):
def getHasMachineQuality(self) -> bool: def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False)) return parseBool(self.getMetaDataEntry("has_machine_quality", False))
## Get default firmware file name if one is specified in the firmware
@pyqtSlot(result = str)
def getDefaultFirmwareName(self) -> str:
machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")
baudrate = 250000
if Platform.isLinux():
# Linux prefers a baudrate of 115200 here because older versions of
# pySerial did not support a baudrate of 250000
baudrate = 115200
# If a firmware file is available, it should be specified in the definition for the printer
hex_file = self.getMetaDataEntry("firmware_file", None)
if machine_has_heated_bed:
hex_file = self.getMetaDataEntry("firmware_hbk_file", hex_file)
if not hex_file:
Logger.log("w", "There is no firmware for machine %s.", self.getBottom().id)
return ""
try:
return Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
except FileNotFoundError:
Logger.log("w", "Firmware file %s not found.", hex_file)
return ""
## private: ## private:
global_stack_mime = MimeType( global_stack_mime = MimeType(

View file

@ -1,6 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import List from typing import List, Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
@ -20,13 +20,18 @@ from UM.Settings.SettingInstance import InstanceState
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack
from UM.Settings.SettingDefinition import SettingDefinition
class SettingInheritanceManager(QObject): class SettingInheritanceManager(QObject):
def __init__(self, parent = None): def __init__(self, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._global_container_stack = None self._global_container_stack = None # type: Optional[ContainerStack]
self._settings_with_inheritance_warning = [] self._settings_with_inheritance_warning = [] # type: List[str]
self._active_container_stack = None self._active_container_stack = None # type: Optional[ExtruderStack]
self._onGlobalContainerChanged() self._onGlobalContainerChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged) ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
@ -41,7 +46,9 @@ class SettingInheritanceManager(QObject):
## Get the keys of all children settings with an override. ## Get the keys of all children settings with an override.
@pyqtSlot(str, result = "QStringList") @pyqtSlot(str, result = "QStringList")
def getChildrenKeysWithOverride(self, key): def getChildrenKeysWithOverride(self, key: str) -> List[str]:
if self._global_container_stack is None:
return []
definitions = self._global_container_stack.definition.findDefinitions(key=key) definitions = self._global_container_stack.definition.findDefinitions(key=key)
if not definitions: if not definitions:
Logger.log("w", "Could not find definition for key [%s]", key) Logger.log("w", "Could not find definition for key [%s]", key)
@ -53,9 +60,11 @@ class SettingInheritanceManager(QObject):
return result return result
@pyqtSlot(str, str, result = "QStringList") @pyqtSlot(str, str, result = "QStringList")
def getOverridesForExtruder(self, key, extruder_index): def getOverridesForExtruder(self, key: str, extruder_index: str) -> List[str]:
result = [] if self._global_container_stack is None:
return []
result = [] # type: List[str]
extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index) extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if not extruder_stack: if not extruder_stack:
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index) Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
@ -73,16 +82,16 @@ class SettingInheritanceManager(QObject):
return result return result
@pyqtSlot(str) @pyqtSlot(str)
def manualRemoveOverride(self, key): def manualRemoveOverride(self, key: str) -> None:
if key in self._settings_with_inheritance_warning: if key in self._settings_with_inheritance_warning:
self._settings_with_inheritance_warning.remove(key) self._settings_with_inheritance_warning.remove(key)
self.settingsWithIntheritanceChanged.emit() self.settingsWithIntheritanceChanged.emit()
@pyqtSlot() @pyqtSlot()
def forceUpdate(self): def forceUpdate(self) -> None:
self._update() self._update()
def _onActiveExtruderChanged(self): def _onActiveExtruderChanged(self) -> None:
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack() new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if not new_active_stack: if not new_active_stack:
self._active_container_stack = None self._active_container_stack = None
@ -94,13 +103,14 @@ class SettingInheritanceManager(QObject):
self._active_container_stack.containersChanged.disconnect(self._onContainersChanged) self._active_container_stack.containersChanged.disconnect(self._onContainersChanged)
self._active_container_stack = new_active_stack self._active_container_stack = new_active_stack
if self._active_container_stack is not None:
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged) self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
self._active_container_stack.containersChanged.connect(self._onContainersChanged) self._active_container_stack.containersChanged.connect(self._onContainersChanged)
self._update() # Ensure that the settings_with_inheritance_warning list is populated. self._update() # Ensure that the settings_with_inheritance_warning list is populated.
def _onPropertyChanged(self, key, property_name): def _onPropertyChanged(self, key: str, property_name: str) -> None:
if (property_name == "value" or property_name == "enabled") and self._global_container_stack: if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
definitions = self._global_container_stack.definition.findDefinitions(key = key) definitions = self._global_container_stack.definition.findDefinitions(key = key) # type: List["SettingDefinition"]
if not definitions: if not definitions:
return return
@ -139,7 +149,7 @@ class SettingInheritanceManager(QObject):
if settings_with_inheritance_warning_changed: if settings_with_inheritance_warning_changed:
self.settingsWithIntheritanceChanged.emit() self.settingsWithIntheritanceChanged.emit()
def _recursiveCheck(self, definition): def _recursiveCheck(self, definition: "SettingDefinition") -> bool:
for child in definition.children: for child in definition.children:
if child.key in self._settings_with_inheritance_warning: if child.key in self._settings_with_inheritance_warning:
return True return True
@ -149,7 +159,7 @@ class SettingInheritanceManager(QObject):
return False return False
@pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged) @pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged)
def settingsWithInheritanceWarning(self): def settingsWithInheritanceWarning(self) -> List[str]:
return self._settings_with_inheritance_warning return self._settings_with_inheritance_warning
## Check if a setting has an inheritance function that is overwritten ## Check if a setting has an inheritance function that is overwritten
@ -159,6 +169,11 @@ class SettingInheritanceManager(QObject):
stack = self._active_container_stack stack = self._active_container_stack
if not stack: # No active container stack yet! if not stack: # No active container stack yet!
return False return False
if self._active_container_stack is None:
return False
all_keys = self._active_container_stack.getAllKeys()
containers = [] # type: List[ContainerInterface] containers = [] # type: List[ContainerInterface]
## Check if the setting has a user state. If not, it is never overwritten. ## Check if the setting has a user state. If not, it is never overwritten.
@ -190,7 +205,7 @@ class SettingInheritanceManager(QObject):
has_setting_function = isinstance(value, SettingFunction) has_setting_function = isinstance(value, SettingFunction)
if has_setting_function: if has_setting_function:
for setting_key in value.getUsedSettingKeys(): for setting_key in value.getUsedSettingKeys():
if setting_key in self._active_container_stack.getAllKeys(): if setting_key in all_keys:
break # We found an actual setting. So has_setting_function can remain true break # We found an actual setting. So has_setting_function can remain true
else: else:
# All of the setting_keys turned out to not be setting keys at all! # All of the setting_keys turned out to not be setting keys at all!
@ -205,7 +220,7 @@ class SettingInheritanceManager(QObject):
break # There is a setting function somewhere, stop looking deeper. break # There is a setting function somewhere, stop looking deeper.
return has_setting_function and has_non_function_value return has_setting_function and has_non_function_value
def _update(self): def _update(self) -> None:
self._settings_with_inheritance_warning = [] # Reset previous data. self._settings_with_inheritance_warning = [] # Reset previous data.
# Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late. # Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late.
@ -226,7 +241,7 @@ class SettingInheritanceManager(QObject):
# Notify others that things have changed. # Notify others that things have changed.
self.settingsWithIntheritanceChanged.emit() self.settingsWithIntheritanceChanged.emit()
def _onGlobalContainerChanged(self): def _onGlobalContainerChanged(self) -> None:
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged) self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)

View file

@ -0,0 +1,90 @@
import os
import urllib.parse
from configparser import ConfigParser
from typing import List
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
from UM.Logger import Logger
from UM.MimeTypeDatabase import MimeTypeDatabase
class SettingVisibilityPreset(QObject):
onSettingsChanged = pyqtSignal()
onNameChanged = pyqtSignal()
onWeightChanged = pyqtSignal()
onIdChanged = pyqtSignal()
def __init__(self, preset_id: str = "", name: str = "", weight: int = 0, parent = None) -> None:
super().__init__(parent)
self._settings = [] # type: List[str]
self._id = preset_id
self._weight = weight
self._name = name
@pyqtProperty("QStringList", notify = onSettingsChanged)
def settings(self) -> List[str]:
return self._settings
@pyqtProperty(str, notify = onIdChanged)
def presetId(self) -> str:
return self._id
@pyqtProperty(int, notify = onWeightChanged)
def weight(self) -> int:
return self._weight
@pyqtProperty(str, notify = onNameChanged)
def name(self) -> str:
return self._name
def setName(self, name: str) -> None:
if name != self._name:
self._name = name
self.onNameChanged.emit()
def setId(self, id: str) -> None:
if id != self._id:
self._id = id
self.onIdChanged.emit()
def setWeight(self, weight: int) -> None:
if weight != self._weight:
self._weight = weight
self.onWeightChanged.emit()
def setSettings(self, settings: List[str]) -> None:
if set(settings) != set(self._settings):
self._settings = list(set(settings)) # filter out non unique
self.onSettingsChanged.emit()
# Load a preset from file. We expect a file that can be parsed by means of the config parser.
# The sections indicate the categories and the parameters placed in it (which don't need values) are the settings
# that should be considered visible.
def loadFromFile(self, file_path: str) -> None:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
if not os.path.isfile(file_path):
Logger.log("e", "[%s] is not a file", file_path)
return None
parser = ConfigParser(interpolation = None, allow_no_value = True) # Accept options without any value,
parser.read([file_path])
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
return None
settings = [] # type: List[str]
for section in parser.sections():
if section == "general":
continue
settings.append(section)
for option in parser[section].keys():
settings.append(option)
self.setSettings(settings)
self.setId(item_id)
self.setName(parser["general"]["name"])
self.setWeight(int(parser["general"]["weight"]))

View file

@ -1,15 +1,17 @@
from UM.Qt.ListModel import ListModel # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from collections import OrderedDict
from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtCore import pyqtSlot, Qt
from UM.Application import Application from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from collections import OrderedDict from UM.Qt.ListModel import ListModel
import os
class UserChangesModel(ListModel): class UserChangesModel(ListModel):
@ -38,9 +40,13 @@ class UserChangesModel(ListModel):
self._update() self._update()
def _update(self): def _update(self):
application = Application.getInstance()
machine_manager = application.getMachineManager()
cura_formula_functions = application.getCuraFormulaFunctions()
item_dict = OrderedDict() item_dict = OrderedDict()
item_list = [] item_list = []
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = machine_manager.activeMachine
if not global_stack: if not global_stack:
return return
@ -71,13 +77,7 @@ class UserChangesModel(ListModel):
# Override "getExtruderValue" with "getDefaultExtruderValue" so we can get the default values # Override "getExtruderValue" with "getDefaultExtruderValue" so we can get the default values
user_changes = containers.pop(0) user_changes = containers.pop(0)
default_value_resolve_context = PropertyEvaluationContext(stack) default_value_resolve_context = cura_formula_functions.createContextForDefaultValueEvaluation(stack)
default_value_resolve_context.context["evaluate_from_container_index"] = 1 # skip the user settings container
default_value_resolve_context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
for setting_key in user_changes.getAllKeys(): for setting_key in user_changes.getAllKeys():
original_value = None original_value = None

View file

@ -12,7 +12,7 @@ from . import ThreeMFWorkspaceWriter
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Platform import Platform from UM.Platform import Platform
i18n_catalog = i18nCatalog("uranium") i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
workspace_extension = "3mf" workspace_extension = "3mf"

View file

@ -3,8 +3,6 @@
from . import ChangeLog from . import ChangeLog
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View file

@ -2,6 +2,7 @@
#Cura is released under the terms of the LGPLv3 or higher. #Cura is released under the terms of the LGPLv3 or higher.
import gc import gc
import sys
from UM.Job import Job from UM.Job import Job
from UM.Application import Application from UM.Application import Application
@ -95,12 +96,16 @@ class ProcessSlicedLayersJob(Job):
layer_count = len(self._layers) layer_count = len(self._layers)
# Find the minimum layer number # Find the minimum layer number
# When disabling the remove empty first layers setting, the minimum layer number will be a positive
# value. In that case the first empty layers will be discarded and start processing layers from the
# first layer with data.
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
# instead simply offset all other layers so the lowest layer is always 0. It could happens that # simply offset all other layers so the lowest layer is always 0. It could happens that the first
# the first raft layer has value -8 but there are just 4 raft (negative) layers. # raft layer has value -8 but there are just 4 raft (negative) layers.
min_layer_number = 0 min_layer_number = sys.maxsize
negative_layers = 0 negative_layers = 0
for layer in self._layers: for layer in self._layers:
if layer.repeatedMessageCount("path_segment") > 0:
if layer.id < min_layer_number: if layer.id < min_layer_number:
min_layer_number = layer.id min_layer_number = layer.id
if layer.id < 0: if layer.id < 0:
@ -109,9 +114,17 @@ class ProcessSlicedLayersJob(Job):
current_layer = 0 current_layer = 0
for layer in self._layers: for layer in self._layers:
# Negative layers are offset by the minimum layer number, but the positive layers are just # If the layer is below the minimum, it means that there is no data, so that we don't create a layer
# offset by the number of negative layers so there is no layer gap between raft and model # data. However, if there are empty layers in between, we compute them.
abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers if layer.id < min_layer_number:
continue
# Layers are offset by the minimum layer number. In case the raft (negative layers) is being used,
# then the absolute layer number is adjusted by removing the empty layers that can be in between raft
# and the model
abs_layer_number = layer.id - min_layer_number
if layer.id >= 0 and negative_layers != 0:
abs_layer_number += (min_layer_number + negative_layers)
layer_data.addLayer(abs_layer_number) layer_data.addLayer(abs_layer_number)
this_layer = layer_data.getLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number)

View file

@ -41,11 +41,15 @@ class StartJobResult(IntEnum):
## Formatter class that handles token expansion in start/end gcode ## Formatter class that handles token expansion in start/end gcode
class GcodeStartEndFormatter(Formatter): class GcodeStartEndFormatter(Formatter):
def get_value(self, key: str, args: str, kwargs: dict, default_extruder_nr: str = "-1") -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class] def __init__(self, default_extruder_nr: int = -1) -> None:
super().__init__()
self._default_extruder_nr = default_extruder_nr
def get_value(self, key: str, args: str, kwargs: dict) -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class]
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key), # The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
# and a default_extruder_nr to use when no extruder_nr is specified # and a default_extruder_nr to use when no extruder_nr is specified
extruder_nr = int(default_extruder_nr) extruder_nr = self._default_extruder_nr
key_fragments = [fragment.strip() for fragment in key.split(",")] key_fragments = [fragment.strip() for fragment in key.split(",")]
if len(key_fragments) == 2: if len(key_fragments) == 2:
@ -247,7 +251,10 @@ class StartSliceJob(Job):
self._buildGlobalInheritsStackMessage(stack) self._buildGlobalInheritsStackMessage(stack)
# Build messages for extruder stacks # Build messages for extruder stacks
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()): # Send the extruder settings in the order of extruder positions. Somehow, if you send e.g. extruder 3 first,
# then CuraEngine can slice with the wrong settings. This I think should be fixed in CuraEngine as well.
extruder_stack_list = sorted(list(global_stack.extruders.items()), key = lambda item: int(item[0]))
for _, extruder_stack in extruder_stack_list:
self._buildExtruderMessage(extruder_stack) self._buildExtruderMessage(extruder_stack)
for group in filtered_object_groups: for group in filtered_object_groups:
@ -339,7 +346,7 @@ class StartSliceJob(Job):
try: try:
# any setting can be used as a token # any setting can be used as a token
fmt = GcodeStartEndFormatter() fmt = GcodeStartEndFormatter(default_extruder_nr = default_extruder_nr)
settings = self._all_extruders_settings.copy() settings = self._all_extruders_settings.copy()
settings["default_extruder_nr"] = default_extruder_nr settings["default_extruder_nr"] = default_extruder_nr
return str(fmt.format(value, **settings)) return str(fmt.format(value, **settings))

View file

@ -1,9 +1,12 @@
# Copyright (c) 2017 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.
import os
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from typing import Set
from UM.Extension import Extension from UM.Extension import Extension
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
@ -13,6 +16,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -21,32 +25,31 @@ i18n_catalog = i18nCatalog("cura")
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy # The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications. # to change it to work for other applications.
class FirmwareUpdateChecker(Extension): class FirmwareUpdateChecker(Extension):
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources"
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
# Initialize the Preference called `latest_checked_firmware` that stores the last version
# checked for the UM3. In the future if we need to check other printers' firmware
Application.getInstance().getPreferences().addPreference("info/latest_checked_firmware", "")
# Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the # Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the
# 'check for updates' option # "check for updates" option
Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True) Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True)
if Application.getInstance().getPreferences().getValue("info/automatic_update_check"): if Application.getInstance().getPreferences().getValue("info/automatic_update_check"):
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded) ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
self._download_url = None
self._check_job = None self._check_job = None
self._checked_printer_names = set() # type: Set[str]
## Callback for the message that is spawned when there is a new version. ## Callback for the message that is spawned when there is a new version.
def _onActionTriggered(self, message, action): def _onActionTriggered(self, message, action):
if action == "download": if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
if self._download_url is not None: machine_id = message.getMachineId()
QDesktopServices.openUrl(QUrl(self._download_url)) download_url = message.getDownloadUrl()
if download_url is not None:
def _onSetDownloadUrl(self, download_url): if QDesktopServices.openUrl(QUrl(download_url)):
self._download_url = download_url Logger.log("i", "Redirected browser to {0} to show newly available firmware.".format(download_url))
else:
Logger.log("e", "Can't reach URL: {0}".format(download_url))
else:
Logger.log("e", "Can't find URL for {0}".format(machine_id))
def _onContainerAdded(self, container): def _onContainerAdded(self, container):
# Only take care when a new GlobalStack was added # Only take care when a new GlobalStack was added
@ -63,13 +66,18 @@ class FirmwareUpdateChecker(Extension):
# \param silent type(boolean) Suppresses messages other than "new version found" messages. # \param silent type(boolean) Suppresses messages other than "new version found" messages.
# This is used when checking for a new firmware version at startup. # This is used when checking for a new firmware version at startup.
def checkFirmwareVersion(self, container = None, silent = False): def checkFirmwareVersion(self, container = None, silent = False):
# Do not run multiple check jobs in parallel container_name = container.definition.getName()
if self._check_job is not None: if container_name in self._checked_printer_names:
Logger.log("i", "A firmware update check is already running, do nothing.") return
self._checked_printer_names.add(container_name)
metadata = container.definition.getMetaData().get("firmware_update_info")
if metadata is None:
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name))
return return
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL, self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent,
callback = self._onActionTriggered, machine_name = container_name, metadata = metadata,
set_download_url_callback = self._onSetDownloadUrl) callback = self._onActionTriggered)
self._check_job.start() self._check_job.start()
self._check_job.finished.connect(self._onJobFinished) self._check_job.finished.connect(self._onJobFinished)

View file

@ -1,13 +1,18 @@
# Copyright (c) 2017 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 UM.Application import Application from UM.Application import Application
from UM.Message import Message from UM.Message import Message
from UM.Logger import Logger from UM.Logger import Logger
from UM.Job import Job from UM.Job import Job
from UM.Version import Version
import urllib.request import urllib.request
import codecs from urllib.error import URLError
from typing import Dict, Optional
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -15,46 +20,86 @@ i18n_catalog = i18nCatalog("cura")
## This job checks if there is an update available on the provided URL. ## This job checks if there is an update available on the provided URL.
class FirmwareUpdateCheckerJob(Job): class FirmwareUpdateCheckerJob(Job):
def __init__(self, container = None, silent = False, url = None, callback = None, set_download_url_callback = None): STRING_ZERO_VERSION = "0.0.0"
STRING_EPSILON_VERSION = "0.0.1"
ZERO_VERSION = Version(STRING_ZERO_VERSION)
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
def __init__(self, container, silent, machine_name, metadata, callback) -> None:
super().__init__() super().__init__()
self._container = container self._container = container
self.silent = silent self.silent = silent
self._url = url
self._callback = callback self._callback = callback
self._set_download_url_callback = set_download_url_callback
def run(self): self._machine_name = machine_name
if not self._url: self._metadata = metadata
Logger.log("e", "Can not check for a new release. URL not set!") self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup]
return self._headers = {} # type:Dict[str, str] # Don't set headers yet.
def getUrlResponse(self, url: str) -> str:
result = self.STRING_ZERO_VERSION
try: try:
request = urllib.request.Request(url, headers = self._headers)
response = urllib.request.urlopen(request)
result = response.read().decode("utf-8")
except URLError:
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
return result
def parseVersionResponse(self, response: str) -> Version:
raw_str = response.split("\n", 1)[0].rstrip()
return Version(raw_str)
def getCurrentVersion(self) -> Version:
max_version = self.ZERO_VERSION
if self._lookups is None:
return max_version
machine_urls = self._lookups.getCheckUrls()
if machine_urls is not None:
for url in machine_urls:
version = self.parseVersionResponse(self.getUrlResponse(url))
if version > max_version:
max_version = version
if max_version < self.EPSILON_VERSION:
Logger.log("w", "MachineID {0} not handled!".format(self._lookups.getMachineName()))
return max_version
def run(self):
if self._lookups is None:
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
try:
# Initialize a Preference that stores the last version checked for this printer.
Application.getInstance().getPreferences().addPreference(
getSettingsKeyForMachine(self._lookups.getMachineId()), "")
# Get headers
application_name = Application.getInstance().getApplicationName() application_name = Application.getInstance().getApplicationName()
headers = {"User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion())} application_version = Application.getInstance().getVersion()
request = urllib.request.Request(self._url, headers = headers) self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
current_version_file = urllib.request.urlopen(request)
reader = codecs.getreader("utf-8")
# get machine name from the definition container # get machine name from the definition container
machine_name = self._container.definition.getName() machine_name = self._container.definition.getName()
machine_name_parts = machine_name.lower().split(" ")
# If it is not None, then we compare between the checked_version and the current_version # If it is not None, then we compare between the checked_version and the current_version
# Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any machine_id = self._lookups.getMachineId()
# other Ultimaker 3 that will come in the future if machine_id is not None:
if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]: Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name))
Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!")
# Nothing to parse, just get the string current_version = self.getCurrentVersion()
# TODO: In the future may be done by parsing a JSON file with diferent version for each printer model
current_version = reader(current_version_file).readline().rstrip()
# If it is the first time the version is checked, the checked_version is '' # If it is the first time the version is checked, the checked_version is ""
checked_version = Application.getInstance().getPreferences().getValue("info/latest_checked_firmware") setting_key_str = getSettingsKeyForMachine(machine_id)
checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str))
# If the checked_version is '', it's because is the first time we check firmware and in this case # If the checked_version is "", it's because is the first time we check firmware and in this case
# we will not show the notification, but we will store it for the next time # we will not show the notification, but we will store it for the next time
Application.getInstance().getPreferences().setValue("info/latest_checked_firmware", current_version) Application.getInstance().getPreferences().setValue(setting_key_str, current_version)
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version) Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version)
# The first time we want to store the current version, the notification will not be shown, # The first time we want to store the current version, the notification will not be shown,
@ -62,28 +107,11 @@ class FirmwareUpdateCheckerJob(Job):
# 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") Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl())
message = Message(i18n_catalog.i18nc(
"@info Don't translate {machine_name}, since it gets replaced by a printer name!",
"New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(
machine_name=machine_name),
title=i18n_catalog.i18nc(
"@info:title The %s gets replaced with the printer name.",
"New %s firmware available") % machine_name)
message.addAction("download",
i18n_catalog.i18nc("@action:button", "How to update"),
"[no_icon]",
"[no_description]",
button_style=Message.ActionButtonStyle.LINK,
button_align=Message.ActionButtonStyle.BUTTON_ALIGN_LEFT)
# If we do this in a cool way, the download url should be available in the JSON file
if self._set_download_url_callback:
self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware")
message.actionTriggered.connect(self._callback) message.actionTriggered.connect(self._callback)
message.show() message.show()
else:
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name))
except Exception as e: except Exception as e:
Logger.log("w", "Failed to check for new version: %s", e) Logger.log("w", "Failed to check for new version: %s", e)

View file

@ -0,0 +1,35 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Optional
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getSettingsKeyForMachine(machine_id: int) -> str:
return "info/latest_checked_firmware_for_{0}".format(machine_id)
class FirmwareUpdateCheckerLookup:
def __init__(self, machine_name, machine_json) -> None:
# Parse all the needed lookup-tables from the ".json" file(s) in the resources folder.
self._machine_id = machine_json.get("id")
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
self._check_urls = [] # type:List[str]
for check_url in machine_json.get("check_urls"):
self._check_urls.append(check_url)
self._redirect_user = machine_json.get("update_url")
def getMachineId(self) -> Optional[int]:
return self._machine_id
def getMachineName(self) -> Optional[int]:
return self._machine_name
def getCheckUrls(self) -> Optional[List[str]]:
return self._check_urls
def getRedirectUserUrl(self) -> Optional[str]:
return self._redirect_user

View file

@ -0,0 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from UM.Message import Message
i18n_catalog = i18nCatalog("cura")
# Make a separate class, since we need an extra field: The machine-id that this message is about.
class FirmwareUpdateCheckerMessage(Message):
STR_ACTION_DOWNLOAD = "download"
def __init__(self, machine_id: int, machine_name: str, download_url: str) -> None:
super().__init__(i18n_catalog.i18nc(
"@info Don't translate {machine_name}, since it gets replaced by a printer name!",
"New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(
machine_name = machine_name),
title = i18n_catalog.i18nc(
"@info:title The %s gets replaced with the printer name.",
"New %s firmware available") % machine_name)
self._machine_id = machine_id
self._download_url = download_url
self.addAction(self.STR_ACTION_DOWNLOAD,
i18n_catalog.i18nc("@action:button", "How to update"),
"[no_icon]",
"[no_description]",
button_style = Message.ActionButtonStyle.LINK,
button_align = Message.ActionButtonStyle.BUTTON_ALIGN_LEFT)
def getMachineId(self) -> int:
return self._machine_id
def getDownloadUrl(self) -> str:
return self._download_url

View file

@ -1,12 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from . import FirmwareUpdateChecker from . import FirmwareUpdateChecker
i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View file

@ -0,0 +1,69 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.CuraApplication import CuraApplication
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdateState
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject
from typing import Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from UM.Settings.ContainerInterface import ContainerInterface
catalog = i18nCatalog("cura")
## Upgrade the firmware of a machine by USB with this action.
class FirmwareUpdaterMachineAction(MachineAction):
def __init__(self) -> None:
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware"))
self._qml_url = "FirmwareUpdaterMachineAction.qml"
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
self._active_output_device = None # type: Optional[PrinterOutputDevice]
self._active_firmware_updater = None # type: Optional[FirmwareUpdater]
CuraApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
def _onEngineCreated(self) -> None:
CuraApplication.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
def _onContainerAdded(self, container: "ContainerInterface") -> None:
# Add this action as a supported action to all machine definitions if they support USB connection
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"):
CuraApplication.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
def _onOutputDevicesChanged(self) -> None:
if self._active_output_device and self._active_output_device.activePrinter:
self._active_output_device.activePrinter.getController().canUpdateFirmwareChanged.disconnect(self._onControllerCanUpdateFirmwareChanged)
output_devices = CuraApplication.getInstance().getMachineManager().printerOutputDevices
self._active_output_device = output_devices[0] if output_devices else None
if self._active_output_device and self._active_output_device.activePrinter:
self._active_output_device.activePrinter.getController().canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self.outputDeviceCanUpdateFirmwareChanged.emit()
def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.outputDeviceCanUpdateFirmwareChanged.emit()
outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
def firmwareUpdater(self) -> Optional["FirmwareUpdater"]:
if self._active_output_device and self._active_output_device.activePrinter.getController().can_update_firmware:
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
return self._active_firmware_updater
elif self._active_firmware_updater and self._active_firmware_updater.firmwareUpdateState not in [FirmwareUpdateState.idle, FirmwareUpdateState.completed]:
# During a firmware update, the PrinterOutputDevice is disconnected but the FirmwareUpdater is still there
return self._active_firmware_updater
self._active_firmware_updater = None
return None

View file

@ -0,0 +1,191 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick.Dialogs 1.2 // For filedialog
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
property bool printerConnected: Cura.MachineManager.printerConnected
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
property bool canUpdateFirmware: activeOutputDevice ? activeOutputDevice.activePrinter.canUpdateFirmware : false
Column
{
id: firmwareUpdaterMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name:"cura"}
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width
text: catalog.i18nc("@title", "Update Firmware")
wrapMode: Text.WordWrap
font.pointSize: 18
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware is the piece of software running directly on your 3D printer. This firmware controls the step motors, regulates the temperature and ultimately makes your printer work.")
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "The firmware shipping with new printers works, but new versions tend to have more features and improvements.");
}
Row
{
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
property string firmwareName: Cura.MachineManager.activeMachine.getDefaultFirmwareName()
Button
{
id: autoUpgradeButton
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
enabled: parent.firmwareName != "" && canUpdateFirmware
onClicked:
{
updateProgressDialog.visible = true;
activeOutputDevice.updateFirmware(parent.firmwareName);
}
}
Button
{
id: manualUpgradeButton
text: catalog.i18nc("@action:button", "Upload custom Firmware");
enabled: canUpdateFirmware
onClicked:
{
customFirmwareDialog.open()
}
}
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
visible: !printerConnected && !updateProgressDialog.visible
text: catalog.i18nc("@label", "Firmware can not be updated because there is no connection with the printer.");
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
visible: printerConnected && !canUpdateFirmware
text: catalog.i18nc("@label", "Firmware can not be updated because the connection with the printer does not support upgrading firmware.");
}
}
FileDialog
{
id: customFirmwareDialog
title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)"
selectExisting: true
onAccepted:
{
updateProgressDialog.visible = true;
activeOutputDevice.updateFirmware(fileUrl);
}
}
UM.Dialog
{
id: updateProgressDialog
width: minimumWidth
minimumWidth: 500 * screenScaleFactor
height: minimumHeight
minimumHeight: 100 * screenScaleFactor
modality: Qt.ApplicationModal
title: catalog.i18nc("@title:window","Firmware Update")
Column
{
anchors.fill: parent
Label
{
anchors
{
left: parent.left
right: parent.right
}
text: {
if(manager.firmwareUpdater == null)
{
return "";
}
switch (manager.firmwareUpdater.firmwareUpdateState)
{
case 0:
return ""; //Not doing anything (eg; idling)
case 1:
return catalog.i18nc("@label","Updating firmware.");
case 2:
return catalog.i18nc("@label","Firmware update completed.");
case 3:
return catalog.i18nc("@label","Firmware update failed due to an unknown error.");
case 4:
return catalog.i18nc("@label","Firmware update failed due to an communication error.");
case 5:
return catalog.i18nc("@label","Firmware update failed due to an input/output error.");
case 6:
return catalog.i18nc("@label","Firmware update failed due to missing firmware.");
}
}
wrapMode: Text.Wrap
}
ProgressBar
{
id: prog
value: (manager.firmwareUpdater != null) ? manager.firmwareUpdater.firmwareProgress : 0
minimumValue: 0
maximumValue: 100
indeterminate:
{
if(manager.firmwareUpdater == null)
{
return false;
}
return manager.firmwareUpdater.firmwareProgress < 1 && manager.firmwareUpdater.firmwareProgress > 0;
}
anchors
{
left: parent.left;
right: parent.right;
}
}
}
rightButtons: [
Button
{
text: catalog.i18nc("@action:button","Close");
enabled: (manager.firmwareUpdater != null) ? manager.firmwareUpdater.firmwareUpdateState != 1 : true;
onClicked: updateProgressDialog.visible = false;
}
]
}
}

View file

@ -0,0 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FirmwareUpdaterMachineAction
def getMetaData():
return {}
def register(app):
return { "machine_action": [
FirmwareUpdaterMachineAction.FirmwareUpdaterMachineAction()
]}

View file

@ -0,0 +1,8 @@
{
"name": "Firmware Updater",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a machine actions for updating firmware.",
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -44,6 +44,7 @@ class FlavorParser:
self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset] self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default self._current_layer_thickness = 0.2 # default
self._filament_diameter = 2.85 # default self._filament_diameter = 2.85 # default
self._previous_extrusion_value = 0.0 # keep track of the filament retractions
CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True) CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
@ -182,6 +183,7 @@ class FlavorParser:
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
if new_extrusion_value > e[self._extruder_number]: if new_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
self._previous_extrusion_value = new_extrusion_value
else: else:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = new_extrusion_value e[self._extruder_number] = new_extrusion_value
@ -191,6 +193,12 @@ class FlavorParser:
if z > self._previous_z and (z - self._previous_z < 1.5): if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z self._previous_z = z
elif self._previous_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
# This case only for initial start, for the first coordinate in GCode
elif e[self._extruder_number] == 0 and self._previous_extrusion_value == 0:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
else: else:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType]) path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
return self._position(x, y, z, f, e) return self._position(x, y, z, f, e)
@ -235,6 +243,7 @@ class FlavorParser:
position.e) position.e)
def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position: def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
self._previous_extrusion_value = 0.0
func = getattr(self, "_gCode%s" % G, None) func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any) line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None: if func is not None:

View file

@ -3,8 +3,6 @@
from . import MachineSettingsAction from . import MachineSettingsAction
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View file

@ -1,11 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# This example is released under the terms of the AGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import ModelChecker from . import ModelChecker
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View file

@ -3,6 +3,7 @@
from . import MonitorStage from . import MonitorStage
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")

View file

@ -62,6 +62,7 @@ UM.Dialog
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: base.textMargin anchors.rightMargin: base.textMargin
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
elide: Text.ElideRight
} }
ListView ListView
{ {
@ -115,6 +116,7 @@ UM.Dialog
{ {
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: control.text text: control.text
elide: Text.ElideRight
color: activeScriptButton.checked ? palette.highlightedText : palette.text color: activeScriptButton.checked ? palette.highlightedText : palette.text
} }
} }
@ -275,6 +277,7 @@ UM.Dialog
anchors.leftMargin: base.textMargin anchors.leftMargin: base.textMargin
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: base.textMargin anchors.rightMargin: base.textMargin
elide: Text.ElideRight
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")

View file

@ -2,8 +2,8 @@
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from . import PostProcessingPlugin from . import PostProcessingPlugin
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View file

@ -3,12 +3,10 @@
from UM.Platform import Platform from UM.Platform import Platform
from UM.Logger import Logger from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {}
}
def register(app): def register(app):
if Platform.isWindows(): if Platform.isWindows():

View file

@ -1,12 +1,11 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import SliceInfo from . import SliceInfo
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {}
}
def register(app): def register(app):
return { "extension": SliceInfo.SliceInfo()} return { "extension": SliceInfo.SliceInfo()}

View file

@ -2,9 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from .src import DiscoverUM3Action from .src import DiscoverUM3Action
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from .src import UM3OutputDevicePlugin from .src import UM3OutputDevicePlugin
def getMetaData(): def getMetaData():

View file

@ -100,8 +100,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
title=i18n_catalog.i18nc("@info:title", title=i18n_catalog.i18nc("@info:title",
"Authentication status")) "Authentication status"))
self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""), self._authentication_failed_message = Message("", title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
self._authentication_failed_message.actionTriggered.connect(self._messageCallback) self._authentication_failed_message.actionTriggered.connect(self._messageCallback)

View file

@ -0,0 +1,68 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater, FirmwareUpdateState
from .avr_isp import stk500v2, intelHex
from serial import SerialException
from time import sleep
MYPY = False
if MYPY:
from cura.PrinterOutputDevice import PrinterOutputDevice
class AvrFirmwareUpdater(FirmwareUpdater):
def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__(output_device)
def _updateFirmware(self) -> None:
try:
hex_file = intelHex.readHex(self._firmware_file)
assert len(hex_file) > 0
except (FileNotFoundError, AssertionError):
Logger.log("e", "Unable to read provided hex file. Could not update firmware.")
self._setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
programmer = stk500v2.Stk500v2()
programmer.progress_callback = self._onFirmwareProgress
# Ensure that other connections are closed.
if self._output_device.isConnected():
self._output_device.close()
try:
programmer.connect(self._output_device._serial_port)
except:
programmer.close()
Logger.logException("e", "Failed to update firmware")
self._setFirmwareUpdateState(FirmwareUpdateState.communication_error)
return
# Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
sleep(1)
if not programmer.isConnected():
Logger.log("e", "Unable to connect with serial. Could not update firmware")
self._setFirmwareUpdateState(FirmwareUpdateState.communication_error)
try:
programmer.programChip(hex_file)
except SerialException as e:
Logger.log("e", "A serial port exception occured during firmware update: %s" % e)
self._setFirmwareUpdateState(FirmwareUpdateState.io_error)
return
except Exception as e:
Logger.log("e", "An unknown exception occured during firmware update: %s" % e)
self._setFirmwareUpdateState(FirmwareUpdateState.unknown_error)
return
programmer.close()
# Try to re-connect with the machine again, which must be done on the Qt thread, so we use call later.
CuraApplication.getInstance().callLater(self._output_device.connect)
self._cleanupAfterUpdate()

View file

@ -1,89 +0,0 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Window 2.2
import QtQuick.Controls 1.2
import UM 1.1 as UM
UM.Dialog
{
id: base;
width: minimumWidth;
minimumWidth: 500 * screenScaleFactor;
height: minimumHeight;
minimumHeight: 100 * screenScaleFactor;
visible: true;
modality: Qt.ApplicationModal;
title: catalog.i18nc("@title:window","Firmware Update");
Column
{
anchors.fill: parent;
Label
{
anchors
{
left: parent.left;
right: parent.right;
}
text: {
switch (manager.firmwareUpdateState)
{
case 0:
return "" //Not doing anything (eg; idling)
case 1:
return catalog.i18nc("@label","Updating firmware.")
case 2:
return catalog.i18nc("@label","Firmware update completed.")
case 3:
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
case 4:
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
case 5:
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
case 6:
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
}
}
wrapMode: Text.Wrap;
}
ProgressBar
{
id: prog
value: manager.firmwareProgress
minimumValue: 0
maximumValue: 100
indeterminate: manager.firmwareProgress < 1 && manager.firmwareProgress > 0
anchors
{
left: parent.left;
right: parent.right;
}
}
SystemPalette
{
id: palette;
}
UM.I18nCatalog { id: catalog; name: "cura"; }
}
rightButtons: [
Button
{
text: catalog.i18nc("@action:button","Close");
enabled: manager.firmwareUpdateCompleteStatus;
onClicked: base.visible = false;
}
]
}

View file

@ -4,7 +4,6 @@
from UM.Logger import Logger from UM.Logger import Logger
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Qt.Duration import DurationFormat from UM.Qt.Duration import DurationFormat
from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
@ -13,28 +12,21 @@ from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.GenericOutputController import GenericOutputController from cura.PrinterOutput.GenericOutputController import GenericOutputController
from .AutoDetectBaudJob import AutoDetectBaudJob from .AutoDetectBaudJob import AutoDetectBaudJob
from .avr_isp import stk500v2, intelHex from .AvrFirmwareUpdater import AvrFirmwareUpdater
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QUrl
from serial import Serial, SerialException, SerialTimeoutException from serial import Serial, SerialException, SerialTimeoutException
from threading import Thread, Event from threading import Thread, Event
from time import time, sleep from time import time
from queue import Queue from queue import Queue
from enum import IntEnum
from typing import Union, Optional, List, cast from typing import Union, Optional, List, cast
import re import re
import functools # Used for reduce import functools # Used for reduce
import os
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class USBPrinterOutputDevice(PrinterOutputDevice): class USBPrinterOutputDevice(PrinterOutputDevice):
firmwareProgressChanged = pyqtSignal()
firmwareUpdateStateChanged = pyqtSignal()
def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None: def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None:
super().__init__(serial_port) super().__init__(serial_port)
self.setName(catalog.i18nc("@item:inmenu", "USB printing")) self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
@ -61,8 +53,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block.
self._update_thread = Thread(target = self._update, daemon = True) self._update_thread = Thread(target = self._update, daemon = True)
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon = True)
self._last_temperature_request = None # type: Optional[int] self._last_temperature_request = None # type: Optional[int]
self._is_printing = False # A print is being sent. self._is_printing = False # A print is being sent.
@ -76,11 +66,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._paused = False self._paused = False
self._printer_busy = False # when printer is preheating and waiting (M190/M109), or when waiting for action on the printer self._printer_busy = False # when printer is preheating and waiting (M190/M109), or when waiting for action on the printer
self._firmware_view = None
self._firmware_location = None
self._firmware_progress = 0
self._firmware_update_state = FirmwareUpdateState.idle
self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB")) self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB"))
# Queue for commands that need to be sent. # Queue for commands that need to be sent.
@ -89,6 +74,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._command_received = Event() self._command_received = Event()
self._command_received.set() self._command_received.set()
self._firmware_updater = AvrFirmwareUpdater(self)
CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit) CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit)
# This is a callback function that checks if there is any printing in progress via USB when the application tries # This is a callback function that checks if there is any printing in progress via USB when the application tries
@ -110,7 +97,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Reset USB device settings ## Reset USB device settings
# #
def resetDeviceSettings(self): def resetDeviceSettings(self) -> None:
self._firmware_name = None self._firmware_name = None
## Request the current scene to be sent to a USB-connected printer. ## Request the current scene to be sent to a USB-connected printer.
@ -136,93 +123,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._printGCode(gcode_list) self._printGCode(gcode_list)
## Show firmware interface.
# This will create the view if its not already created.
def showFirmwareInterface(self):
if self._firmware_view is None:
path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
self._firmware_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
self._firmware_view.show()
@pyqtSlot(str)
def updateFirmware(self, file):
# the file path could be url-encoded.
if file.startswith("file://"):
self._firmware_location = QUrl(file).toLocalFile()
else:
self._firmware_location = file
self.showFirmwareInterface()
self.setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()
def _updateFirmware(self):
# Ensure that other connections are closed.
if self._connection_state != ConnectionState.closed:
self.close()
try:
hex_file = intelHex.readHex(self._firmware_location)
assert len(hex_file) > 0
except (FileNotFoundError, AssertionError):
Logger.log("e", "Unable to read provided hex file. Could not update firmware.")
self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
programmer = stk500v2.Stk500v2()
programmer.progress_callback = self._onFirmwareProgress
try:
programmer.connect(self._serial_port)
except:
programmer.close()
Logger.logException("e", "Failed to update firmware")
self.setFirmwareUpdateState(FirmwareUpdateState.communication_error)
return
# Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
sleep(1)
if not programmer.isConnected():
Logger.log("e", "Unable to connect with serial. Could not update firmware")
self.setFirmwareUpdateState(FirmwareUpdateState.communication_error)
try:
programmer.programChip(hex_file)
except SerialException:
self.setFirmwareUpdateState(FirmwareUpdateState.io_error)
return
except:
self.setFirmwareUpdateState(FirmwareUpdateState.unknown_error)
return
programmer.close()
# Clean up for next attempt.
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_location = ""
self._onFirmwareProgress(100)
self.setFirmwareUpdateState(FirmwareUpdateState.completed)
# Try to re-connect with the machine again, which must be done on the Qt thread, so we use call later.
CuraApplication.getInstance().callLater(self.connect)
@pyqtProperty(float, notify = firmwareProgressChanged)
def firmwareProgress(self):
return self._firmware_progress
@pyqtProperty(int, notify=firmwareUpdateStateChanged)
def firmwareUpdateState(self):
return self._firmware_update_state
def setFirmwareUpdateState(self, state):
if self._firmware_update_state != state:
self._firmware_update_state = state
self.firmwareUpdateStateChanged.emit()
# Callback function for firmware update progress.
def _onFirmwareProgress(self, progress, max_progress = 100):
self._firmware_progress = (progress / max_progress) * 100 # Convert to scale of 0-100
self.firmwareProgressChanged.emit()
## Start a print based on a g-code. ## Start a print based on a g-code.
# \param gcode_list List with gcode (strings). # \param gcode_list List with gcode (strings).
def _printGCode(self, gcode_list: List[str]): def _printGCode(self, gcode_list: List[str]):
@ -273,13 +173,19 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
except SerialException: except SerialException:
Logger.log("w", "An exception occured while trying to create serial connection") Logger.log("w", "An exception occured while trying to create serial connection")
return return
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
self._onGlobalContainerStackChanged()
self.setConnectionState(ConnectionState.connected)
self._update_thread.start()
def _onGlobalContainerStackChanged(self):
container_stack = CuraApplication.getInstance().getGlobalContainerStack() container_stack = CuraApplication.getInstance().getGlobalContainerStack()
num_extruders = container_stack.getProperty("machine_extruder_count", "value") num_extruders = container_stack.getProperty("machine_extruder_count", "value")
# Ensure that a printer is created. # Ensure that a printer is created.
self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)] controller = GenericOutputController(self)
controller.setCanUpdateFirmware(True)
self._printers = [PrinterOutputModel(output_controller = controller, number_of_extruders = num_extruders)]
self._printers[0].updateName(container_stack.getName()) self._printers[0].updateName(container_stack.getName())
self.setConnectionState(ConnectionState.connected)
self._update_thread.start()
def close(self): def close(self):
super().close() super().close()
@ -296,6 +202,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._command_queue.put(command) self._command_queue.put(command)
else: else:
self._sendCommand(command) self._sendCommand(command)
def _sendCommand(self, command: Union[str, bytes]): def _sendCommand(self, command: Union[str, bytes]):
if self._serial is None or self._connection_state != ConnectionState.connected: if self._serial is None or self._connection_state != ConnectionState.connected:
return return
@ -452,7 +359,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
elapsed_time = int(time() - self._print_start_time) elapsed_time = int(time() - self._print_start_time)
print_job = self._printers[0].activePrintJob print_job = self._printers[0].activePrintJob
if print_job is None: if print_job is None:
print_job = PrintJobOutputModel(output_controller = GenericOutputController(self), name= CuraApplication.getInstance().getPrintInformation().jobName) controller = GenericOutputController(self)
controller.setCanUpdateFirmware(True)
print_job = PrintJobOutputModel(output_controller=controller, name=CuraApplication.getInstance().getPrintInformation().jobName)
print_job.updateState("printing") print_job.updateState("printing")
self._printers[0].updateActivePrintJob(print_job) self._printers[0].updateActivePrintJob(print_job)
@ -463,13 +372,3 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
print_job.updateTimeTotal(estimated_time) print_job.updateTimeTotal(estimated_time)
self._gcode_position += 1 self._gcode_position += 1
class FirmwareUpdateState(IntEnum):
idle = 0
updating = 1
completed = 2
unknown_error = 3
communication_error = 4
io_error = 5
firmware_not_found_error = 6

View file

@ -2,14 +2,12 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import threading import threading
import platform
import time import time
import serial.tools.list_ports import serial.tools.list_ports
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Logger import Logger from UM.Logger import Logger
from UM.Resources import Resources
from UM.Signal import Signal, signalemitter from UM.Signal import Signal, signalemitter
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -87,65 +85,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
self._addRemovePorts(port_list) self._addRemovePorts(port_list)
time.sleep(5) time.sleep(5)
@pyqtSlot(result = str)
def getDefaultFirmwareName(self):
# Check if there is a valid global container stack
global_container_stack = self._application.getGlobalContainerStack()
if not global_container_stack:
Logger.log("e", "There is no global container stack. Can not update firmware.")
self._firmware_view.close()
return ""
# The bottom of the containerstack is the machine definition
machine_id = global_container_stack.getBottom().id
machine_has_heated_bed = global_container_stack.getProperty("machine_heated_bed", "value")
if platform.system() == "Linux":
baudrate = 115200
else:
baudrate = 250000
# NOTE: The keyword used here is the id of the machine. You can find the id of your machine in the *.json file, eg.
# https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json#L2
# The *.hex files are stored at a seperate repository:
# https://github.com/Ultimaker/cura-binary-data/tree/master/cura/resources/firmware
machine_without_extras = {"bq_witbox" : "MarlinWitbox.hex",
"bq_hephestos_2" : "MarlinHephestos2.hex",
"ultimaker_original" : "MarlinUltimaker-{baudrate}.hex",
"ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex",
"ultimaker_original_dual" : "MarlinUltimaker-{baudrate}-dual.hex",
"ultimaker2" : "MarlinUltimaker2.hex",
"ultimaker2_go" : "MarlinUltimaker2go.hex",
"ultimaker2_plus" : "MarlinUltimaker2plus.hex",
"ultimaker2_extended" : "MarlinUltimaker2extended.hex",
"ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex",
}
machine_with_heated_bed = {"ultimaker_original" : "MarlinUltimaker-HBK-{baudrate}.hex",
"ultimaker_original_dual" : "MarlinUltimaker-HBK-{baudrate}-dual.hex",
}
##TODO: Add check for multiple extruders
hex_file = None
if machine_id in machine_without_extras.keys(): # The machine needs to be defined here!
if machine_id in machine_with_heated_bed.keys() and machine_has_heated_bed:
Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_id)
hex_file = machine_with_heated_bed[machine_id] # Return firmware with heated bed enabled
else:
Logger.log("d", "Choosing basic firmware for machine %s.", machine_id)
hex_file = machine_without_extras[machine_id] # Return "basic" firmware
else:
Logger.log("w", "There is no firmware for machine %s.", machine_id)
if hex_file:
try:
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
except FileNotFoundError:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
else:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
## Helper to identify serial ports (and scan for them) ## Helper to identify serial ports (and scan for them)
def _addRemovePorts(self, serial_ports): def _addRemovePorts(self, serial_ports):
# First, find and add all new or changed keys # First, find and add all new or changed keys

View file

@ -2,9 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import USBPrinterOutputDeviceManager from . import USBPrinterOutputDeviceManager
from PyQt5.QtQml import qmlRegisterSingletonType
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
@ -14,5 +11,4 @@ def getMetaData():
def register(app): def register(app):
# We are violating the QT API here (as we use a factory, which is technically not allowed). # We are violating the QT API here (as we use a factory, which is technically not allowed).
# but we don't really have another means for doing this (and it seems to you know -work-) # but we don't really have another means for doing this (and it seems to you know -work-)
qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager(app)} return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager(app)}

View file

@ -17,7 +17,7 @@ Cura.MachineAction
property int rightRow: (checkupMachineAction.width * 0.60) | 0 property int rightRow: (checkupMachineAction.width * 0.60) | 0
property bool heatupHotendStarted: false property bool heatupHotendStarted: false
property bool heatupBedStarted: false property bool heatupBedStarted: false
property bool usbConnected: Cura.USBPrinterManager.connectedPrinterList.rowCount() > 0 property bool printerConnected: Cura.MachineManager.printerConnected
UM.I18nCatalog { id: catalog; name:"cura"} UM.I18nCatalog { id: catalog; name:"cura"}
Label Label
@ -86,7 +86,7 @@ Cura.MachineAction
anchors.left: connectionLabel.right anchors.left: connectionLabel.right
anchors.top: parent.top anchors.top: parent.top
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: checkupMachineAction.usbConnected ? catalog.i18nc("@info:status","Connected"): catalog.i18nc("@info:status","Not connected") text: checkupMachineAction.printerConnected ? catalog.i18nc("@info:status","Connected"): catalog.i18nc("@info:status","Not connected")
} }
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
Label Label
@ -97,7 +97,7 @@ Cura.MachineAction
anchors.top: connectionLabel.bottom anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop X: ") text: catalog.i18nc("@label","Min endstop X: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -107,7 +107,7 @@ Cura.MachineAction
anchors.top: connectionLabel.bottom anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked") text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
Label Label
@ -118,7 +118,7 @@ Cura.MachineAction
anchors.top: endstopXLabel.bottom anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Y: ") text: catalog.i18nc("@label","Min endstop Y: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -128,7 +128,7 @@ Cura.MachineAction
anchors.top: endstopXLabel.bottom anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked") text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
Label Label
@ -139,7 +139,7 @@ Cura.MachineAction
anchors.top: endstopYLabel.bottom anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Z: ") text: catalog.i18nc("@label","Min endstop Z: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -149,7 +149,7 @@ Cura.MachineAction
anchors.top: endstopYLabel.bottom anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked") text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
Label Label
@ -161,7 +161,7 @@ Cura.MachineAction
anchors.top: endstopZLabel.bottom anchors.top: endstopZLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Nozzle temperature check: ") text: catalog.i18nc("@label","Nozzle temperature check: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -171,7 +171,7 @@ Cura.MachineAction
anchors.left: nozzleTempLabel.right anchors.left: nozzleTempLabel.right
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@info:status","Not checked") text: catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Item Item
{ {
@ -181,7 +181,7 @@ Cura.MachineAction
anchors.top: nozzleTempLabel.top anchors.top: nozzleTempLabel.top
anchors.left: bedTempStatus.right anchors.left: bedTempStatus.right
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2) anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
Button Button
{ {
text: checkupMachineAction.heatupHotendStarted ? catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating") text: checkupMachineAction.heatupHotendStarted ? catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
@ -209,7 +209,7 @@ Cura.MachineAction
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.hotendTemperature + "°C" text: manager.hotendTemperature + "°C"
font.bold: true font.bold: true
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
Label Label
@ -221,7 +221,7 @@ Cura.MachineAction
anchors.top: nozzleTempLabel.bottom anchors.top: nozzleTempLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Build plate temperature check:") text: catalog.i18nc("@label","Build plate temperature check:")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
} }
Label Label
@ -232,7 +232,7 @@ Cura.MachineAction
anchors.left: bedTempLabel.right anchors.left: bedTempLabel.right
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked") text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
} }
Item Item
{ {
@ -242,7 +242,7 @@ Cura.MachineAction
anchors.top: bedTempLabel.top anchors.top: bedTempLabel.top
anchors.left: bedTempStatus.right anchors.left: bedTempStatus.right
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2) anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
Button Button
{ {
text: checkupMachineAction.heatupBedStarted ?catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating") text: checkupMachineAction.heatupBedStarted ?catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
@ -270,7 +270,7 @@ Cura.MachineAction
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.bedTemperature + "°C" text: manager.bedTemperature + "°C"
font.bold: true font.bold: true
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
} }
Label Label
{ {

View file

@ -1,19 +0,0 @@
from UM.Application import Application
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
catalog = i18nCatalog("cura")
## Upgrade the firmware of a machine by USB with this action.
class UpgradeFirmwareMachineAction(MachineAction):
def __init__(self):
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Upgrade Firmware"))
self._qml_url = "UpgradeFirmwareMachineAction.qml"
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
def _onContainerAdded(self, container):
# Add this action as a supported action to all machine definitions if they support USB connection
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"):
Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())

View file

@ -1,93 +0,0 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick.Dialogs 1.2 // For filedialog
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
property bool printerConnected: Cura.MachineManager.printerConnected
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
Item
{
id: upgradeFirmwareMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name:"cura"}
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Upgrade Firmware")
wrapMode: Text.WordWrap
font.pointSize: 18
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware is the piece of software running directly on your 3D printer. This firmware controls the step motors, regulates the temperature and ultimately makes your printer work.")
}
Label
{
id: upgradeText1
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "The firmware shipping with new printers works, but new versions tend to have more features and improvements.");
}
Row
{
anchors.top: upgradeText1.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
property var firmwareName: Cura.USBPrinterManager.getDefaultFirmwareName()
Button
{
id: autoUpgradeButton
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
enabled: parent.firmwareName != "" && activeOutputDevice
onClicked:
{
activeOutputDevice.updateFirmware(parent.firmwareName)
}
}
Button
{
id: manualUpgradeButton
text: catalog.i18nc("@action:button", "Upload custom Firmware");
enabled: activeOutputDevice != null
onClicked:
{
customFirmwareDialog.open()
}
}
}
FileDialog
{
id: customFirmwareDialog
title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)"
selectExisting: true
onAccepted: activeOutputDevice.updateFirmware(fileUrl)
}
}
}

View file

@ -1,22 +1,16 @@
# Copyright (c) 2016 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 . import BedLevelMachineAction from . import BedLevelMachineAction
from . import UpgradeFirmwareMachineAction
from . import UMOUpgradeSelection from . import UMOUpgradeSelection
from . import UM2UpgradeSelection from . import UM2UpgradeSelection
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {}
}
def register(app): def register(app):
return { "machine_action": [ return { "machine_action": [
BedLevelMachineAction.BedLevelMachineAction(), BedLevelMachineAction.BedLevelMachineAction(),
UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(),
UMOUpgradeSelection.UMOUpgradeSelection(), UMOUpgradeSelection.UMOUpgradeSelection(),
UM2UpgradeSelection.UM2UpgradeSelection() UM2UpgradeSelection.UM2UpgradeSelection()
]} ]}

View file

@ -3,9 +3,6 @@
from . import VersionUpgrade21to22 from . import VersionUpgrade21to22
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade21to22.VersionUpgrade21to22() upgrade = VersionUpgrade21to22.VersionUpgrade21to22()
def getMetaData(): def getMetaData():

View file

@ -3,9 +3,6 @@
from . import VersionUpgrade from . import VersionUpgrade
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade.VersionUpgrade22to24() upgrade = VersionUpgrade.VersionUpgrade22to24()
def getMetaData(): def getMetaData():

View file

@ -3,9 +3,6 @@
from . import VersionUpgrade25to26 from . import VersionUpgrade25to26
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade25to26.VersionUpgrade25to26() upgrade = VersionUpgrade25to26.VersionUpgrade25to26()
def getMetaData(): def getMetaData():

View file

@ -3,9 +3,6 @@
from . import VersionUpgrade26to27 from . import VersionUpgrade26to27
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade26to27.VersionUpgrade26to27() upgrade = VersionUpgrade26to27.VersionUpgrade26to27()
def getMetaData(): def getMetaData():

View file

@ -5,7 +5,6 @@ from . import VersionUpgrade33to34
upgrade = VersionUpgrade33to34.VersionUpgrade33to34() upgrade = VersionUpgrade33to34.VersionUpgrade33to34()
def getMetaData(): def getMetaData():
return { return {
"version_upgrade": { "version_upgrade": {

View file

@ -5,7 +5,6 @@ from . import VersionUpgrade34to35
upgrade = VersionUpgrade34to35.VersionUpgrade34to35() upgrade = VersionUpgrade34to35.VersionUpgrade34to35()
def getMetaData(): def getMetaData():
return { return {
"version_upgrade": { "version_upgrade": {

View file

@ -5,10 +5,7 @@ from . import XmlMaterialProfile
from . import XmlMaterialUpgrader from . import XmlMaterialUpgrader
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrader = XmlMaterialUpgrader.XmlMaterialUpgrader() upgrader = XmlMaterialUpgrader.XmlMaterialUpgrader()

View file

@ -118,6 +118,23 @@
} }
} }
}, },
"FirmwareUpdater": {
"package_info": {
"package_id": "FirmwareUpdater",
"package_type": "plugin",
"display_name": "Firmware Updater",
"description": "Provides a machine actions for updating firmware.",
"package_version": "1.0.0",
"sdk_version": 5,
"website": "https://ultimaker.com",
"author": {
"author_id": "Ultimaker",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
},
"GCodeGzReader": { "GCodeGzReader": {
"package_info": { "package_info": {
"package_id": "GCodeGzReader", "package_id": "GCodeGzReader",

View file

@ -12,7 +12,8 @@
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "bq_hephestos_extruder_0" "0": "bq_hephestos_extruder_0"
} },
"firmware_file": "MarlinHephestos2.hex"
}, },
"overrides": { "overrides": {

View file

@ -12,7 +12,8 @@
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "bq_witbox_extruder_0" "0": "bq_witbox_extruder_0"
} },
"firmware_file": "MarlinWitbox.hex"
}, },
"overrides": { "overrides": {

View file

@ -9,7 +9,8 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "creality_ender3_platform.stl", "platform": "creality_ender3_platform.stl",
"preferred_quality_type": "draft", "preferred_quality_type": "draft",
"machine_extruder_trains": { "machine_extruder_trains":
{
"0": "creality_ender3_extruder_0" "0": "creality_ender3_extruder_0"
} }
}, },
@ -40,6 +41,9 @@
[30, 34] [30, 34]
] ]
}, },
"material_diameter": {
"default_value": 1.75
},
"acceleration_enabled": { "acceleration_enabled": {
"default_value": true "default_value": true
}, },
@ -55,6 +59,9 @@
"jerk_travel": { "jerk_travel": {
"default_value": 20 "default_value": 20
}, },
"layer_height": {
"default_value": 0.10
},
"layer_height_0": { "layer_height_0": {
"default_value": 0.2 "default_value": 0.2
}, },

View file

@ -1217,11 +1217,11 @@
"connect_skin_polygons": "connect_skin_polygons":
{ {
"label": "Connect Top/Bottom Polygons", "label": "Connect Top/Bottom Polygons",
"description": "Connect top/bottom skin paths where they run next to each other. For the concentric pattern enabling this setting greatly reduces the travel time, but because the connections can happend midway over infill this feature can reduce the top surface quality.", "description": "Connect top/bottom skin paths where they run next to each other. For the concentric pattern enabling this setting greatly reduces the travel time, but because the connections can happen midway over infill this feature can reduce the top surface quality.",
"type": "bool", "type": "bool",
"default_value": false, "default_value": false,
"enabled": "(top_layers > 0 or bottom_layers > 0) and top_bottom_pattern == 'concentric'", "enabled": "(top_layers > 0 or bottom_layers > 0) and top_bottom_pattern == 'concentric'",
"limit_to_extruder": "infill_extruder_nr", "limit_to_extruder": "top_bottom_extruder_nr",
"settable_per_mesh": true "settable_per_mesh": true
}, },
"skin_angles": "skin_angles":
@ -3918,6 +3918,48 @@
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
}, },
"support_brim_enable":
{
"label": "Enable Support Brim",
"description": "Generate a brim within the support infill regions of the first layer. This brim is printed underneath the support, not around it. Enabling this setting increases the adhesion of support to the build plate.",
"type": "bool",
"default_value": false,
"enabled": "support_enable or support_tree_enable",
"limit_to_extruder": "support_infill_extruder_nr",
"settable_per_mesh": false,
"settable_per_extruder": true
},
"support_brim_width":
{
"label": "Support Brim Width",
"description": "The width of the brim to print underneath the support. A larger brim enhances adhesion to the build plate, at the cost of some extra material.",
"type": "float",
"unit": "mm",
"default_value": 8.0,
"minimum_value": "0.0",
"maximum_value_warning": "50.0",
"enabled": "support_enable",
"settable_per_mesh": false,
"settable_per_extruder": true,
"limit_to_extruder": "support_infill_extruder_nr",
"children":
{
"support_brim_line_count":
{
"label": "Support Brim Line Count",
"description": "The number of lines used for the support brim. More brim lines enhance adhesion to the build plate, at the cost of some extra material.",
"type": "int",
"default_value": 20,
"minimum_value": "0",
"maximum_value_warning": "50 / skirt_brim_line_width",
"value": "math.ceil(support_brim_width / (skirt_brim_line_width * initial_layer_line_width_factor / 100.0))",
"enabled": "support_enable",
"settable_per_mesh": false,
"settable_per_extruder": true,
"limit_to_extruder": "support_infill_extruder_nr"
}
}
},
"support_z_distance": "support_z_distance":
{ {
"label": "Support Z Distance", "label": "Support Z Distance",
@ -4568,6 +4610,17 @@
} }
} }
}, },
"brim_replaces_support":
{
"label": "Brim Replaces Support",
"description": "Enforce brim to be printed around the model even if that space would otherwise be occupied by support. This replaces some regions of the first layer of support by brim regions.",
"type": "bool",
"default_value": true,
"enabled": "resolveOrValue('adhesion_type') == 'brim' and support_enable",
"settable_per_mesh": false,
"settable_per_extruder": true,
"limit_to_extruder": "adhesion_extruder_nr"
},
"brim_outside_only": "brim_outside_only":
{ {
"label": "Brim Only on Outside", "label": "Brim Only on Outside",

View file

@ -8,7 +8,6 @@
"manufacturer": "NA", "manufacturer": "NA",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"has_materials": false, "has_materials": false,
"supported_actions": [ "MachineSettingsAction", "UpgradeFirmware" ],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "makeit_l_dual_1st", "0": "makeit_l_dual_1st",

View file

@ -8,7 +8,6 @@
"manufacturer": "NA", "manufacturer": "NA",
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"has_materials": false, "has_materials": false,
"supported_actions": [ "MachineSettingsAction", "UpgradeFirmware" ],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "makeit_dual_1st", "0": "makeit_dual_1st",

View file

@ -6,6 +6,7 @@
"visible": true, "visible": true,
"author": "Ultimaker", "author": "Ultimaker",
"manufacturer": "MakerBot", "manufacturer": "MakerBot",
"machine_x3g_variant": "r1",
"file_formats": "application/x3g", "file_formats": "application/x3g",
"platform_offset": [ 0, 0, 0], "platform_offset": [ 0, 0, 0],
"machine_extruder_trains": "machine_extruder_trains":

View file

@ -10,7 +10,6 @@
"platform": "tam_series1.stl", "platform": "tam_series1.stl",
"platform_offset": [-580.0, -6.23, 253.5], "platform_offset": [-580.0, -6.23, 253.5],
"has_materials": false, "has_materials": false,
"supported_actions": ["UpgradeFirmware"],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "tam_extruder_0" "0": "tam_extruder_0"

View file

@ -17,11 +17,12 @@
"preferred_variant_name": "0.4 mm", "preferred_variant_name": "0.4 mm",
"exclude_materials": ["generic_hips", "generic_petg", "generic_bam", "ultimaker_bam", "generic_pva", "ultimaker_pva", "generic_tough_pla", "ultimaker_tough_pla_black", "ultimaker_tough_pla_green", "ultimaker_tough_pla_red", "ultimaker_tough_pla_white"], "exclude_materials": ["generic_hips", "generic_petg", "generic_bam", "ultimaker_bam", "generic_pva", "ultimaker_pva", "generic_tough_pla", "ultimaker_tough_pla_black", "ultimaker_tough_pla_green", "ultimaker_tough_pla_red", "ultimaker_tough_pla_white"],
"first_start_actions": ["UM2UpgradeSelection"], "first_start_actions": ["UM2UpgradeSelection"],
"supported_actions":["UM2UpgradeSelection", "UpgradeFirmware"], "supported_actions":["UM2UpgradeSelection"],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "ultimaker2_extruder_0" "0": "ultimaker2_extruder_0"
} },
"firmware_file": "MarlinUltimaker2.hex"
}, },
"overrides": { "overrides": {
"machine_name": { "default_value": "Ultimaker 2" }, "machine_name": { "default_value": "Ultimaker 2" },

View file

@ -14,7 +14,8 @@
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "ultimaker2_extended_extruder_0" "0": "ultimaker2_extended_extruder_0"
} },
"firmware_file": "MarlinUltimaker2extended.hex"
}, },
"overrides": { "overrides": {

View file

@ -10,11 +10,11 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "ultimaker2_platform.obj", "platform": "ultimaker2_platform.obj",
"platform_texture": "Ultimaker2ExtendedPlusbackplate.png", "platform_texture": "Ultimaker2ExtendedPlusbackplate.png",
"supported_actions": ["UpgradeFirmware"],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "ultimaker2_extended_plus_extruder_0" "0": "ultimaker2_extended_plus_extruder_0"
} },
"firmware_file": "MarlinUltimaker2extended-plus.hex"
}, },
"overrides": { "overrides": {

View file

@ -13,11 +13,11 @@
"platform_texture": "Ultimaker2Gobackplate.png", "platform_texture": "Ultimaker2Gobackplate.png",
"platform_offset": [0, 0, 0], "platform_offset": [0, 0, 0],
"first_start_actions": [], "first_start_actions": [],
"supported_actions": ["UpgradeFirmware"],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "ultimaker2_go_extruder_0" "0": "ultimaker2_go_extruder_0"
} },
"firmware_file": "MarlinUltimaker2go.hex"
}, },
"overrides": { "overrides": {

View file

@ -15,11 +15,11 @@
"has_machine_materials": true, "has_machine_materials": true,
"has_machine_quality": true, "has_machine_quality": true,
"first_start_actions": [], "first_start_actions": [],
"supported_actions": ["UpgradeFirmware"],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "ultimaker2_plus_extruder_0" "0": "ultimaker2_plus_extruder_0"
} },
"firmware_file": "MarlinUltimaker2plus.hex"
}, },
"overrides": { "overrides": {

View file

@ -24,7 +24,16 @@
}, },
"first_start_actions": [ "DiscoverUM3Action" ], "first_start_actions": [ "DiscoverUM3Action" ],
"supported_actions": [ "DiscoverUM3Action" ], "supported_actions": [ "DiscoverUM3Action" ],
"supports_usb_connection": false "supports_usb_connection": false,
"firmware_update_info": {
"id": 9066,
"check_urls":
[
"http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources",
"http://software.ultimaker.com/releases/firmware/9066/stable/version.txt"
],
"update_url": "https://ultimaker.com/firmware"
}
}, },

View file

@ -23,7 +23,16 @@
"1": "ultimaker3_extended_extruder_right" "1": "ultimaker3_extended_extruder_right"
}, },
"first_start_actions": [ "DiscoverUM3Action" ], "first_start_actions": [ "DiscoverUM3Action" ],
"supported_actions": [ "DiscoverUM3Action" ] "supported_actions": [ "DiscoverUM3Action" ],
"firmware_update_info": {
"id": 9511,
"check_urls":
[
"http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources",
"http://software.ultimaker.com/releases/firmware/9511/stable/version.txt"
],
"update_url": "https://ultimaker.com/firmware"
}
}, },
"overrides": { "overrides": {

View file

@ -14,11 +14,13 @@
"has_machine_quality": true, "has_machine_quality": true,
"exclude_materials": ["generic_hips", "generic_petg", "generic_bam", "ultimaker_bam", "generic_pva", "ultimaker_pva", "generic_tough_pla", "ultimaker_tough_pla_black", "ultimaker_tough_pla_green", "ultimaker_tough_pla_red", "ultimaker_tough_pla_white"], "exclude_materials": ["generic_hips", "generic_petg", "generic_bam", "ultimaker_bam", "generic_pva", "ultimaker_pva", "generic_tough_pla", "ultimaker_tough_pla_black", "ultimaker_tough_pla_green", "ultimaker_tough_pla_red", "ultimaker_tough_pla_white"],
"first_start_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel"], "first_start_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel"],
"supported_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel", "UpgradeFirmware"], "supported_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel"],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "ultimaker_original_extruder_0" "0": "ultimaker_original_extruder_0"
} },
"firmware_file": "MarlinUltimaker-{baudrate}.hex",
"firmware_hbk_file": "MarlinUltimaker-HBK-{baudrate}.hex"
}, },
"overrides": { "overrides": {

View file

@ -19,8 +19,10 @@
"0": "ultimaker_original_dual_1st", "0": "ultimaker_original_dual_1st",
"1": "ultimaker_original_dual_2nd" "1": "ultimaker_original_dual_2nd"
}, },
"firmware_file": "MarlinUltimaker-{baudrate}-dual.hex",
"firmware_hbk_file": "MarlinUltimaker-HBK-{baudrate}-dual.hex",
"first_start_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel"], "first_start_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel"],
"supported_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel", "UpgradeFirmware"] "supported_actions": ["UMOUpgradeSelection", "UMOCheckup", "BedLevel"]
}, },
"overrides": { "overrides": {

View file

@ -12,11 +12,12 @@
"platform_texture": "UltimakerPlusbackplate.png", "platform_texture": "UltimakerPlusbackplate.png",
"quality_definition": "ultimaker_original", "quality_definition": "ultimaker_original",
"first_start_actions": ["UMOCheckup", "BedLevel"], "first_start_actions": ["UMOCheckup", "BedLevel"],
"supported_actions": ["UMOCheckup", "BedLevel", "UpgradeFirmware"], "supported_actions": ["UMOCheckup", "BedLevel"],
"machine_extruder_trains": "machine_extruder_trains":
{ {
"0": "ultimaker_original_plus_extruder_0" "0": "ultimaker_original_plus_extruder_0"
} },
"firmware_file": "MarlinUltimaker-UMOP-{baudrate}.hex"
}, },
"overrides": { "overrides": {

View file

@ -30,7 +30,12 @@
"first_start_actions": [ "DiscoverUM3Action" ], "first_start_actions": [ "DiscoverUM3Action" ],
"supported_actions": [ "DiscoverUM3Action" ], "supported_actions": [ "DiscoverUM3Action" ],
"supports_usb_connection": false, "supports_usb_connection": false,
"weight": -1 "weight": -1,
"firmware_update_info": {
"id": 9051,
"check_urls": ["http://software.ultimaker.com/releases/firmware/9051/stable/version.txt"],
"update_url": "https://ultimaker.com/firmware"
}
}, },
"overrides": { "overrides": {
@ -63,7 +68,7 @@
"machine_end_gcode": { "default_value": "" }, "machine_end_gcode": { "default_value": "" },
"prime_tower_position_x": { "default_value": 345 }, "prime_tower_position_x": { "default_value": 345 },
"prime_tower_position_y": { "default_value": 222.5 }, "prime_tower_position_y": { "default_value": 222.5 },
"prime_blob_enable": { "enabled": false }, "prime_blob_enable": { "enabled": true, "default_value": false },
"speed_travel": "speed_travel":
{ {

View file

@ -18,9 +18,6 @@
0, 0,
-28, -28,
0 0
],
"supported_actions": [
"UpgradeFirmware"
] ]
}, },
"overrides": { "overrides": {

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -9,14 +9,8 @@
}, },
"overrides": { "overrides": {
"extruder_nr": { "extruder_nr": { "default_value": 0 },
"default_value": 0 "machine_nozzle_size": { "default_value": 0.4 },
}, "material_diameter": { "default_value": 1.75 }
"machine_nozzle_size": {
"default_value": 0.4
},
"material_diameter": {
"default_value": 1.75
}
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.5 }, "machine_nozzle_size": { "default_value": 0.5 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.5 }, "machine_nozzle_size": { "default_value": 0.5 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -11,6 +11,6 @@
"overrides": { "overrides": {
"extruder_nr": { "default_value": 0 }, "extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 }, "machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 2.85 } "material_diameter": { "default_value": 1.75 }
} }
} }

View file

@ -19,6 +19,18 @@ UM.Dialog
width: minimumWidth width: minimumWidth
height: minimumHeight height: minimumHeight
Rectangle
{
width: parent.width + 2 * margin // margin from Dialog.qml
height: version.y + version.height + margin
anchors.top: parent.top
anchors.topMargin: - margin
anchors.horizontalCenter: parent.horizontalCenter
color: UM.Theme.getColor("viewport_background")
}
Image Image
{ {
id: logo id: logo
@ -42,6 +54,7 @@ UM.Dialog
text: catalog.i18nc("@label","version: %1").arg(UM.Application.version) text: catalog.i18nc("@label","version: %1").arg(UM.Application.version)
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
anchors.right : logo.right anchors.right : logo.right
anchors.top: logo.bottom anchors.top: logo.bottom
anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0 anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0
@ -75,6 +88,7 @@ UM.Dialog
ScrollView ScrollView
{ {
id: credits
anchors.top: creditsNotes.bottom anchors.top: creditsNotes.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
@ -128,7 +142,11 @@ UM.Dialog
projectsModel.append({ name:"SciPy", description: catalog.i18nc("@label", "Support library for scientific computing"), license: "BSD-new", url: "https://www.scipy.org/" }); projectsModel.append({ name:"SciPy", description: catalog.i18nc("@label", "Support library for scientific computing"), license: "BSD-new", url: "https://www.scipy.org/" });
projectsModel.append({ name:"NumPy", description: catalog.i18nc("@label", "Support library for faster math"), license: "BSD", url: "http://www.numpy.org/" }); projectsModel.append({ name:"NumPy", description: catalog.i18nc("@label", "Support library for faster math"), license: "BSD", url: "http://www.numpy.org/" });
projectsModel.append({ name:"NumPy-STL", description: catalog.i18nc("@label", "Support library for handling STL files"), license: "BSD", url: "https://github.com/WoLpH/numpy-stl" }); projectsModel.append({ name:"NumPy-STL", description: catalog.i18nc("@label", "Support library for handling STL files"), license: "BSD", url: "https://github.com/WoLpH/numpy-stl" });
projectsModel.append({ name:"Shapely", description: catalog.i18nc("@label", "Support library for handling planar objects"), license: "BSD", url: "https://github.com/Toblerity/Shapely" });
projectsModel.append({ name:"Trimesh", description: catalog.i18nc("@label", "Support library for handling triangular meshes"), license: "MIT", url: "https://trimsh.org" });
projectsModel.append({ name:"NetworkX", description: catalog.i18nc("@label", "Support library for analysis of complex networks"), license: "3-clause BSD", url: "https://networkx.github.io/" });
projectsModel.append({ name:"libSavitar", description: catalog.i18nc("@label", "Support library for handling 3MF files"), license: "LGPLv3", url: "https://github.com/ultimaker/libsavitar" }); projectsModel.append({ name:"libSavitar", description: catalog.i18nc("@label", "Support library for handling 3MF files"), license: "LGPLv3", url: "https://github.com/ultimaker/libsavitar" });
projectsModel.append({ name:"libCharon", description: catalog.i18nc("@label", "Support library for file metadata and streaming"), license: "LGPLv3", url: "https://github.com/ultimaker/libcharon" });
projectsModel.append({ name:"PySerial", description: catalog.i18nc("@label", "Serial communication library"), license: "Python", url: "http://pyserial.sourceforge.net/" }); projectsModel.append({ name:"PySerial", description: catalog.i18nc("@label", "Serial communication library"), license: "Python", url: "http://pyserial.sourceforge.net/" });
projectsModel.append({ name:"python-zeroconf", description: catalog.i18nc("@label", "ZeroConf discovery library"), license: "LGPL", url: "https://github.com/jstasiak/python-zeroconf" }); projectsModel.append({ name:"python-zeroconf", description: catalog.i18nc("@label", "ZeroConf discovery library"), license: "LGPL", url: "https://github.com/jstasiak/python-zeroconf" });
projectsModel.append({ name:"Clipper", description: catalog.i18nc("@label", "Polygon clipping library"), license: "Boost", url: "http://www.angusj.com/delphi/clipper.php" }); projectsModel.append({ name:"Clipper", description: catalog.i18nc("@label", "Polygon clipping library"), license: "Boost", url: "http://www.angusj.com/delphi/clipper.php" });

View file

@ -18,17 +18,17 @@ Menu
Instantiator Instantiator
{ {
model: settingVisibilityPresetsModel model: settingVisibilityPresetsModel.items
MenuItem MenuItem
{ {
text: model.name text: modelData.name
checkable: true checkable: true
checked: model.id == settingVisibilityPresetsModel.activePreset checked: modelData.presetId == settingVisibilityPresetsModel.activePreset
exclusiveGroup: group exclusiveGroup: group
onTriggered: onTriggered:
{ {
settingVisibilityPresetsModel.setActivePreset(model.id); settingVisibilityPresetsModel.setActivePreset(modelData.presetId);
} }
} }

View file

@ -171,7 +171,7 @@ UM.PreferencesPage
append({ text: "日本語", code: "ja_JP" }) append({ text: "日本語", code: "ja_JP" })
append({ text: "한국어", code: "ko_KR" }) append({ text: "한국어", code: "ko_KR" })
append({ text: "Nederlands", code: "nl_NL" }) append({ text: "Nederlands", code: "nl_NL" })
//Polish is disabled for being incomplete: append({ text: "Polski", code: "pl_PL" }) append({ text: "Polski", code: "pl_PL" })
append({ text: "Português do Brasil", code: "pt_BR" }) append({ text: "Português do Brasil", code: "pt_BR" })
append({ text: "Português", code: "pt_PT" }) append({ text: "Português", code: "pt_PT" })
append({ text: "Русский", code: "ru_RU" }) append({ text: "Русский", code: "ru_RU" })

View file

@ -41,15 +41,7 @@ Rectangle
anchors.left: swatch.right anchors.left: swatch.right
anchors.verticalCenter: materialSlot.verticalCenter anchors.verticalCenter: materialSlot.verticalCenter
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
font.italic: font.italic: Cura.MachineManager.currentRootMaterialId[Cura.ExtruderManager.activeExtruderIndex] == material.root_material_id
{
var selected_material = Cura.MachineManager.currentRootMaterialId[Cura.ExtruderManager.activeExtruderIndex]
if(selected_material == material.root_material_id)
{
return true
}
return false
}
} }
MouseArea MouseArea
{ {

View file

@ -110,24 +110,25 @@ UM.PreferencesPage
right: parent.right right: parent.right
} }
model: settingVisibilityPresetsModel model: settingVisibilityPresetsModel.items
textRole: "name" textRole: "name"
currentIndex: currentIndex:
{ {
// Load previously selected preset. for(var i = 0; i < settingVisibilityPresetsModel.items.length; ++i)
var index = settingVisibilityPresetsModel.find("id", settingVisibilityPresetsModel.activePreset)
if (index == -1)
{ {
return 0 if(settingVisibilityPresetsModel.items[i].id == settingVisibilityPresetsModel.activePreset)
{
currentIndex = i;
return;
} }
}
return index return -1
} }
onActivated: onActivated:
{ {
var preset_id = settingVisibilityPresetsModel.getItem(index).id; var preset_id = settingVisibilityPresetsModel.items[index].id;
settingVisibilityPresetsModel.setActivePreset(preset_id); settingVisibilityPresetsModel.setActivePreset(preset_id);
} }
} }

View file

@ -14,11 +14,19 @@ Item
implicitHeight: Math.floor(childrenRect.height + UM.Theme.getSize("default_margin").height * 2) implicitHeight: Math.floor(childrenRect.height + UM.Theme.getSize("default_margin").height * 2)
property var outputDevice: null property var outputDevice: null
Connections
{
target: Cura.MachineManager
onGlobalContainerChanged:
{
outputDevice = Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null;
}
}
Rectangle Rectangle
{ {
height: childrenRect.height height: childrenRect.height
color: UM.Theme.getColor("setting_category") color: UM.Theme.getColor("setting_category")
property var activePrinter: outputDevice != null ? outputDevice.activePrinter : null
Label Label
{ {
@ -28,7 +36,7 @@ Item
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
anchors.margins: UM.Theme.getSize("default_margin").width anchors.margins: UM.Theme.getSize("default_margin").width
text: outputDevice != null ? activePrinter.name : "" text: outputDevice != null ? outputDevice.activePrinter.name : ""
} }
Label Label

View file

@ -117,7 +117,7 @@ UM.Dialog
height: childrenRect.height height: childrenRect.height
Label Label
{ {
text: catalog.i18nc("@action:label", Cura.MachineManager.activeMachineNetworkGroupName != "" ? "Printer Group" : "Name") text: Cura.MachineManager.activeMachineNetworkGroupName != "" ? catalog.i18nc("@action:label", "Printer Group") : catalog.i18nc("@action:label", "Name")
width: Math.floor(scroll.width / 3) | 0 width: Math.floor(scroll.width / 3) | 0
} }
Label Label

View file

@ -25,7 +25,6 @@ support_angle = 60
support_enable = True support_enable = True
support_interface_enable = True support_interface_enable = True
support_pattern = triangles support_pattern = triangles
support_roof_enable = True
support_type = everywhere support_type = everywhere
support_use_towers = False support_use_towers = False
support_xy_distance = 0.7 support_xy_distance = 0.7

View file

@ -25,7 +25,6 @@ support_angle = 60
support_enable = True support_enable = True
support_interface_enable = True support_interface_enable = True
support_pattern = triangles support_pattern = triangles
support_roof_enable = True
support_type = everywhere support_type = everywhere
support_use_towers = False support_use_towers = False
support_xy_distance = 0.7 support_xy_distance = 0.7

View file

@ -25,7 +25,6 @@ support_angle = 60
support_enable = True support_enable = True
support_interface_enable = True support_interface_enable = True
support_pattern = triangles support_pattern = triangles
support_roof_enable = True
support_type = everywhere support_type = everywhere
support_use_towers = False support_use_towers = False
support_xy_distance = 0.7 support_xy_distance = 0.7

View file

@ -1,239 +0,0 @@
#!/usr/bin/env python3
#
# This script checks the correctness of the list of visibility settings
#
import collections
import configparser
import json
import os
import sys
from typing import Any, Dict, List
# Directory where this python file resides
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
#
# This class
#
class SettingVisibilityInspection:
def __init__(self) -> None:
# The order of settings type. If the setting is in basic list then it also should be in expert
self._setting_visibility_order = ["basic", "advanced", "expert"]
# This is dictionary with categories as keys and all setting keys as values.
self.all_settings_keys = {} # type: Dict[str, List[str]]
# Load all Cura setting keys from the given fdmprinter.json file
def loadAllCuraSettingKeys(self, fdmprinter_json_path: str) -> None:
with open(fdmprinter_json_path, "r", encoding = "utf-8") as f:
json_data = json.load(f)
# Get all settings keys in each category
for key, data in json_data["settings"].items(): # top level settings are categories
if "type" in data and data["type"] == "category":
self.all_settings_keys[key] = []
self._flattenSettings(data["children"], key) # actual settings are children of top level category-settings
def _flattenSettings(self, settings: Dict[str, str], category: str) -> None:
for key, setting in settings.items():
if "type" in setting and setting["type"] != "category":
self.all_settings_keys[category].append(key)
if "children" in setting:
self._flattenSettings(setting["children"], category)
# Loads the given setting visibility file and returns a dict with categories as keys and a list of setting keys as
# values.
def _loadSettingVisibilityConfigFile(self, file_name: str) -> Dict[str, List[str]]:
with open(file_name, "r", encoding = "utf-8") as f:
parser = configparser.ConfigParser(allow_no_value = True)
parser.read_file(f)
data_dict = {}
for category, option_dict in parser.items():
if category in (parser.default_section, "general"):
continue
data_dict[category] = []
for key in option_dict:
data_dict[category].append(key)
return data_dict
def validateSettingsVisibility(self, setting_visibility_files: Dict[str, str]) -> Dict[str, Dict[str, Any]]:
# First load all setting visibility files into the dict "setting_visibility_dict" in the following structure:
# <visibility_name> -> <category> -> <list-fo-setting-keys>
# "basic" -> "info"
setting_visibility_dict = {} # type: Dict[str, Dict[str, List[str]]]
for visibility_name, file_path in setting_visibility_files.items():
setting_visibility_dict[visibility_name] = self._loadSettingVisibilityConfigFile(file_path)
# The result is in the format:
# <visibility_name> -> dict
# "basic" -> "file_name": "basic.cfg"
# "is_valid": True / False
# "invalid_categories": List[str]
# "invalid_settings": Dict[category -> List[str]]
# "missing_categories_from_previous": List[str]
# "missing_settings_from_previous": Dict[category -> List[str]]
all_result_dict = dict() # type: Dict[str, Dict[str, Any]]
previous_result = None
previous_visibility_dict = None
is_all_valid = True
for visibility_name in self._setting_visibility_order:
invalid_categories = []
invalid_settings = collections.defaultdict(list)
this_visibility_dict = setting_visibility_dict[visibility_name]
# Check if categories and keys exist at all
for category, key_list in this_visibility_dict.items():
if category not in self.all_settings_keys:
invalid_categories.append(category)
continue # If this category doesn't exist at all, not need to check for details
for key in key_list:
if key not in self.all_settings_keys[category]:
invalid_settings[category].append(key)
is_settings_valid = len(invalid_categories) == 0 and len(invalid_settings) == 0
file_path = setting_visibility_files[visibility_name]
result_dict = {"file_name": os.path.basename(file_path),
"is_valid": is_settings_valid,
"invalid_categories": invalid_categories,
"invalid_settings": invalid_settings,
"missing_categories_from_previous": list(),
"missing_settings_from_previous": dict(),
}
# If this is not the first item in the list, check if the settings are defined in the previous
# visibility file.
# A visibility with more details SHOULD add more settings. It SHOULD NOT remove any settings defined
# in the less detailed visibility.
if previous_visibility_dict is not None:
missing_categories_from_previous = []
missing_settings_from_previous = collections.defaultdict(list)
for prev_category, prev_key_list in previous_visibility_dict.items():
# Skip the categories that are invalid
if prev_category in previous_result["invalid_categories"]:
continue
if prev_category not in this_visibility_dict:
missing_categories_from_previous.append(prev_category)
continue
this_key_list = this_visibility_dict[prev_category]
for key in prev_key_list:
# Skip the settings that are invalid
if key in previous_result["invalid_settings"][prev_category]:
continue
if key not in this_key_list:
missing_settings_from_previous[prev_category].append(key)
result_dict["missing_categories_from_previous"] = missing_categories_from_previous
result_dict["missing_settings_from_previous"] = missing_settings_from_previous
is_settings_valid = len(missing_categories_from_previous) == 0 and len(missing_settings_from_previous) == 0
result_dict["is_valid"] = result_dict["is_valid"] and is_settings_valid
# Update the complete result dict
all_result_dict[visibility_name] = result_dict
previous_result = result_dict
previous_visibility_dict = this_visibility_dict
is_all_valid = is_all_valid and result_dict["is_valid"]
all_result_dict["all_results"] = {"is_valid": is_all_valid}
return all_result_dict
def printResults(self, all_result_dict: Dict[str, Dict[str, Any]]) -> None:
print("")
print("Setting Visibility Check Results:")
prev_visibility_name = None
for visibility_name in self._setting_visibility_order:
if visibility_name not in all_result_dict:
continue
result_dict = all_result_dict[visibility_name]
print("=============================")
result_str = "OK" if result_dict["is_valid"] else "INVALID"
print("[%s] : [%s] : %s" % (visibility_name, result_dict["file_name"], result_str))
if result_dict["is_valid"]:
continue
# Print details of invalid settings
if result_dict["invalid_categories"]:
print("It has the following non-existing CATEGORIES:")
for category in result_dict["invalid_categories"]:
print(" - [%s]" % category)
if result_dict["invalid_settings"]:
print("")
print("It has the following non-existing SETTINGS:")
for category, key_list in result_dict["invalid_settings"].items():
for key in key_list:
print(" - [%s / %s]" % (category, key))
if prev_visibility_name is not None:
if result_dict["missing_categories_from_previous"]:
print("")
print("The following CATEGORIES are defined in the previous visibility [%s] but not here:" % prev_visibility_name)
for category in result_dict["missing_categories_from_previous"]:
print(" - [%s]" % category)
if result_dict["missing_settings_from_previous"]:
print("")
print("The following SETTINGS are defined in the previous visibility [%s] but not here:" % prev_visibility_name)
for category, key_list in result_dict["missing_settings_from_previous"].items():
for key in key_list:
print(" - [%s / %s]" % (category, key))
print("")
prev_visibility_name = visibility_name
#
# Returns a dictionary of setting visibility .CFG files in the given search directory.
# The dict has the name of the visibility type as the key (such as "basic", "advanced", "expert"), and
# the actual file path (absolute path).
#
def getAllSettingVisiblityFiles(search_dir: str) -> Dict[str, str]:
visibility_file_dict = dict()
extension = ".cfg"
for file_name in os.listdir(search_dir):
file_path = os.path.join(search_dir, file_name)
# Only check files that has the .cfg extension
if not os.path.isfile(file_path):
continue
if not file_path.endswith(extension):
continue
base_filename = os.path.basename(file_name)[:-len(extension)]
visibility_file_dict[base_filename] = file_path
return visibility_file_dict
def main() -> None:
setting_visibility_files_dir = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "resources", "setting_visibility"))
fdmprinter_def_path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "resources", "definitions", "fdmprinter.def.json"))
setting_visibility_files_dict = getAllSettingVisiblityFiles(setting_visibility_files_dir)
inspector = SettingVisibilityInspection()
inspector.loadAllCuraSettingKeys(fdmprinter_def_path)
check_result = inspector.validateSettingsVisibility(setting_visibility_files_dict)
is_result_valid = check_result["all_results"]["is_valid"]
inspector.printResults(check_result)
sys.exit(0 if is_result_valid else 1)
if __name__ == "__main__":
main()

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