Browse Source

Merge branch 'master' into fix_marlin_press_to_resume

fieldOfView 6 years ago
parent
commit
f4c88aff0f

+ 12 - 1
cura/CameraImageProvider.py

@@ -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.QtQuick import QQuickImageProvider
 from PyQt5.QtCore import QSize
 
 from UM.Application import Application
 
-
+##  Creates screenshots of the current scene.
 class CameraImageProvider(QQuickImageProvider):
     def __init__(self):
         super().__init__(QQuickImageProvider.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):
         for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
             try:

+ 17 - 9
cura/CuraApplication.py

@@ -4,7 +4,7 @@
 import os
 import sys
 import time
-from typing import cast, TYPE_CHECKING
+from typing import cast, TYPE_CHECKING, Optional
 
 import numpy
 
@@ -109,6 +109,7 @@ from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisi
 from cura.Settings.ContainerManager import ContainerManager
 from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
 import cura.Settings.cura_empty_instance_containers
+from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
 
 from cura.ObjectsModel import ObjectsModel
 
@@ -176,6 +177,8 @@ class CuraApplication(QtApplication):
 
         self._single_instance = None
 
+        self._cura_formula_functions = None  # type: Optional[CuraFormulaFunctions]
+
         self._cura_package_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
     # SettingDefinition and SettingFunction.
     def __initializeSettingDefinitionsAndFunctions(self):
+        self._cura_formula_functions = CuraFormulaFunctions(self)
+
         # 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_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
@@ -345,10 +350,10 @@ class CuraApplication(QtApplication):
         SettingDefinition.addSettingType("optional_extruder", None, str, None)
         SettingDefinition.addSettingType("[int]", None, str, None)
 
-        SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
-        SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
-        SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
-        SettingFunction.registerOperator("defaultExtruderPosition", ExtruderManager.getDefaultExtruderPosition)
+        SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
+        SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
+        SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
+        SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
 
     # Adds all resources and container related resources.
     def __addAllResourcesAndContainerResources(self) -> None:
@@ -709,10 +714,8 @@ class CuraApplication(QtApplication):
         self._print_information = PrintInformation.PrintInformation(self)
         self._cura_actions = CuraActions.CuraActions(self)
 
-        # Initialize setting visibility presets model
-        self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self)
-        default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
-        self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
+        # Initialize setting visibility presets model.
+        self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
 
         # Initialize Cura API
         self._cura_API.initialize()
@@ -815,6 +818,11 @@ class CuraApplication(QtApplication):
     def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
         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:
         return self._machine_error_checker
 

+ 55 - 82
cura/Machines/Models/SettingVisibilityPresetsModel.py

@@ -1,135 +1,109 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
-from typing import Optional, List, Dict, Union
-import os
-import urllib.parse
-from configparser import ConfigParser
+from typing import Optional, List
 
-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.Qt.ListModel import ListModel
 from UM.Resources import Resources
-from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
 
 from UM.i18n import i18nCatalog
+from cura.Settings.SettingVisibilityPreset import SettingVisibilityPreset
+
 catalog = i18nCatalog("cura")
 
 
-class SettingVisibilityPresetsModel(ListModel):
-    IdRole = Qt.UserRole + 1
-    NameRole = Qt.UserRole + 2
-    SettingsRole = Qt.UserRole + 3
+class SettingVisibilityPresetsModel(QObject):
+    onItemsChanged = pyqtSignal()
+    activePresetChanged = pyqtSignal()
 
-    def __init__(self, parent = None):
+    def __init__(self, preferences, parent = None):
         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()
-        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
         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)
         self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
         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
         visible_settings = self._preferences.getValue("general/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:
             self._onPreferencesChanged("general/visible_settings")
 
         self.activePresetChanged.emit()
 
-    def _getItem(self, item_id: str) -> Optional[dict]:
+    def getVisibilityPresetById(self, item_id: str) -> Optional[SettingVisibilityPreset]:
         result = None
-        for item in self.items:
-            if item["id"] == item_id:
+        for item in self._items:
+            if item.presetId == item_id:
                 result = item
                 break
         return result
 
     def _populate(self) -> None:
         from cura.CuraApplication import CuraApplication
-        items = [] # type: List[Dict[str, Union[str, int, List[str]]]]
-        for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
-            try:
-                mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
-            except MimeTypeNotFoundError:
-                Logger.log("e", "Could not determine mime type of file %s", file_path)
-                continue
+        items = []  # type: List[SettingVisibilityPreset]
 
-            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,
+        custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
+        items.append(custom_preset)
+        for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
+            setting_visibility_preset = SettingVisibilityPreset()
             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,
-                })
-
+                setting_visibility_preset.loadFromFile(file_path)
             except Exception:
                 Logger.logException("e", "Failed to load setting preset %s", file_path)
 
-        items.sort(key = lambda k: (int(k["weight"]), k["id"]))  # type: ignore
-        # Put "custom" at the top
-        items.insert(0, {"id": "custom",
-                         "name": "Custom selection",
-                         "weight": -100,
-                         "settings": []})
+            items.append(setting_visibility_preset)
+
+        # Sort them on weight (and if that fails, use ID)
+        items.sort(key = lambda k: (int(k.weight), k.presetId))
 
         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)
-    def setActivePreset(self, preset_id: str):
-        if preset_id == self._active_preset_item["id"]:
+    def setActivePreset(self, preset_id: str) -> None:
+        if preset_id == self._active_preset_item.presetId:
             Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
             return
 
-        preset_item = None
-        for item in self.items:
-            if item["id"] == preset_id:
-                preset_item = item
-                break
+        preset_item = self.getVisibilityPresetById(preset_id)
         if preset_item is None:
             Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
             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:
             # Save the current visibility settings to custom
             current_visibility_string = self._preferences.getValue("general/visible_settings")
             if 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":
             # Get settings from the stored custom data
             new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
@@ -141,11 +115,9 @@ class SettingVisibilityPresetsModel(ListModel):
         self._active_preset_item = preset_item
         self.activePresetChanged.emit()
 
-    activePresetChanged = pyqtSignal()
-
     @pyqtProperty(str, notify = activePresetChanged)
     def activePreset(self) -> str:
-        return self._active_preset_item["id"]
+        return self._active_preset_item.presetId
 
     def _onPreferencesChanged(self, name: str) -> None:
         if name != "general/visible_settings":
@@ -158,25 +130,26 @@ class SettingVisibilityPresetsModel(ListModel):
 
         visibility_set = set(visibility_string.split(";"))
         matching_preset_item = None
-        for item in self.items:
-            if item["id"] == "custom":
+        for item in self._items:
+            if item.presetId == "custom":
                 continue
-            if set(item["settings"]) == visibility_set:
+            if set(item.settings) == visibility_set:
                 matching_preset_item = item
                 break
 
         item_to_set = self._active_preset_item
         if matching_preset_item is None:
             # 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
                 self._preferences.setValue("cura/custom_visible_settings", visibility_string)
             else:
-                item_to_set = self.items[0]  # 0 is custom
+                # We need to move to custom preset.
+                item_to_set = self.getVisibilityPresetById("custom")
         else:
             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._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()

+ 78 - 0
cura/PrinterOutput/FirmwareUpdater.py

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

+ 37 - 30
cura/PrinterOutput/GenericOutputController.py

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

+ 1 - 3
cura/PrinterOutput/NetworkedPrinterOutputDevice.py

@@ -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
             # sleep.
             if time_since_last_response > self._recreate_network_manager_time:
-                if self._last_manager_create_time is None:
-                    self._createNetworkManager()
-                elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
+                if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
                     self._createNetworkManager()
                 assert(self._manager is not None)
         elif self._connection_state == ConnectionState.closed:

+ 1 - 1
cura/PrinterOutput/PrintJobOutputModel.py

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

+ 25 - 14
cura/PrinterOutput/PrinterOutputController.py

@@ -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.
 
 from UM.Logger import Logger
+from UM.Signal import Signal
+
+from typing import Union
 
 MYPY = False
 if MYPY:
     from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
     from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
     from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+    from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
 
 
 class PrinterOutputController:
-    def __init__(self, output_device):
+    def __init__(self, output_device: "PrinterOutputDevice") -> None:
         self.can_pause = True
         self.can_abort = True
         self.can_pre_heat_bed = True
         self.can_pre_heat_hotends = True
         self.can_send_raw_gcode = True
         self.can_control_manually = True
+        self.can_update_firmware = False
         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")
 
-    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")
 
-    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")
 
-    def cancelPreheatBed(self, printer: "PrinterOutputModel"):
+    def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
         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")
 
-    def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
+    def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
         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")
 
-    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")
 
-    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")
 
-    def homeBed(self, printer: "PrinterOutputModel"):
+    def homeBed(self, printer: "PrinterOutputModel") -> None:
         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")
 
-    def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
+    def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
         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()

+ 62 - 48
cura/PrinterOutput/PrinterOutputModel.py

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

+ 19 - 6
cura/PrinterOutputDevice.py

@@ -4,22 +4,24 @@
 from UM.Decorators import deprecated
 from UM.i18n import i18nCatalog
 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 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.Qt.QtApplication import QtApplication
+from UM.FlameProfiler import pyqtSlot
 
 from enum import IntEnum  # For the connection state tracking.
-from typing import Callable, List, Optional
+from typing import Callable, List, Optional, Union
 
 MYPY = False
 if MYPY:
     from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
     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")
 
@@ -83,6 +85,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
 
         self._connection_state = ConnectionState.closed #type: ConnectionState
 
+        self._firmware_updater = None #type: Optional[FirmwareUpdater]
         self._firmware_name = None #type: Optional[str]
         self._address = "" #type: str
         self._connection_text = "" #type: str
@@ -128,7 +131,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
 
         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")
 
     @pyqtProperty(QObject, notify = printersChanged)
@@ -225,4 +228,14 @@ class PrinterOutputDevice(QObject, OutputDevice):
     #
     #   This name can be used to define device type
     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)

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