Browse Source

Merge branch 'master' of https://github.com/Ultimaker/Cura

fieldOfView 8 years ago
parent
commit
19412c5bda

+ 6 - 0
CMakeLists.txt

@@ -6,6 +6,12 @@ include(GNUInstallDirs)
 
 set(URANIUM_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/../uranium/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
 
+# Tests
+# Note that we use exit 0 here to not mark the build as a failure on test failure
+add_custom_target(tests)
+add_custom_command(TARGET tests POST_BUILD COMMAND "PYTHONPATH=${CMAKE_SOURCE_DIR}/../Uranium/:${CMAKE_SOURCE_DIR}" ${PYTHON_EXECUTABLE} -m pytest -r a --junitxml=${CMAKE_BINARY_DIR}/junit.xml ${CMAKE_SOURCE_DIR} || exit 0)
+
+
 set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
 set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
 configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)

+ 10 - 0
cura/CuraApplication.py

@@ -44,6 +44,7 @@ from . import ZOffsetDecorator
 from . import CuraSplashScreen
 from . import MachineManagerModel
 from . import ContainerSettingsModel
+from . import MachineActionManager
 
 from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from PyQt5.QtGui import QColor, QIcon
@@ -99,6 +100,8 @@ class CuraApplication(QtApplication):
         SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
         SettingDefinition.addSettingType("extruder", int, str, UM.Settings.Validator)
 
+        self._machine_action_manager = MachineActionManager.MachineActionManager()
+
         super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType)
 
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@@ -367,6 +370,7 @@ class CuraApplication(QtApplication):
         qmlRegisterSingletonType(MachineManagerModel.MachineManagerModel, "Cura", 1, 0, "MachineManager",
                                  MachineManagerModel.createMachineManagerModel)
 
+        qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
         self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
         self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
         self.initializeEngine()
@@ -383,6 +387,12 @@ class CuraApplication(QtApplication):
 
             self.exec_()
 
+    ##  Get the machine action manager
+    #   We ignore any *args given to this, as we also register the machine manager as qml singleton.
+    #   It wants to give this function an engine and script engine, but we don't care about that.
+    def getMachineActionManager(self, *args):
+        return self._machine_action_manager
+
     ##   Handle Qt events
     def event(self, event):
         if event.type() == QEvent.FileOpen:

+ 78 - 0
cura/MachineAction.py

@@ -0,0 +1,78 @@
+# Copyright (c) 2016 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
+from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl
+from PyQt5.QtQml import QQmlComponent, QQmlContext
+
+from UM.PluginObject import PluginObject
+from UM.PluginRegistry import PluginRegistry
+
+from UM.Application import Application
+
+import os
+
+
+class MachineAction(QObject, PluginObject):
+    def __init__(self, key, label = ""):
+        super().__init__()
+        self._key = key
+        self._label = label
+        self._qml_url = ""
+
+        self._component = None
+        self._context = None
+        self._view = None
+        self._finished = False
+
+    labelChanged = pyqtSignal()
+    onFinished = pyqtSignal()
+
+    def getKey(self):
+        return self._key
+
+    @pyqtProperty(str, notify = labelChanged)
+    def label(self):
+        return self._label
+
+    def setLabel(self, label):
+        if self._label != label:
+            self._label = label
+            self.labelChanged.emit()
+
+    ##  Reset the action to it's default state.
+    #   This should not be re-implemented by child classes, instead re-implement _reset.
+    #   /sa _reset
+    @pyqtSlot()
+    def reset(self):
+        self._finished = False
+        self._reset()
+
+    ##  Protected implementation of reset.
+    #   /sa reset()
+    def _reset(self):
+        pass
+
+    @pyqtSlot()
+    def setFinished(self):
+        self._finished = True
+        self._reset()
+        self.onFinished.emit()
+
+    @pyqtProperty(bool, notify = onFinished)
+    def finished(self):
+        return self._finished
+
+    def _createViewFromQML(self):
+        path = QUrl.fromLocalFile(
+            os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url))
+        self._component = QQmlComponent(Application.getInstance()._engine, path)
+        self._context = QQmlContext(Application.getInstance()._engine.rootContext())
+        self._context.setContextProperty("manager", self)
+        self._view = self._component.create(self._context)
+
+    @pyqtProperty(QObject, constant = True)
+    def displayItem(self):
+        if not self._component:
+            self._createViewFromQML()
+
+        return self._view

+ 143 - 0
cura/MachineActionManager.py

@@ -0,0 +1,143 @@
+# Copyright (c) 2016 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+from UM.Logger import Logger
+from UM.PluginRegistry import PluginRegistry  # So MachineAction can be added as plugin type
+
+from UM.Settings.ContainerRegistry import ContainerRegistry
+from UM.Settings.DefinitionContainer import DefinitionContainer
+
+from PyQt5.QtCore import QObject, pyqtSlot
+
+##  Raised when trying to add an unknown machine action as a required action
+class UnknownMachineActionError(Exception):
+    pass
+
+
+##  Raised when trying to add a machine action that does not have an unique key.
+class NotUniqueMachineActionError(Exception):
+    pass
+
+
+class MachineActionManager(QObject):
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self._machine_actions = {}  # Dict of all known machine actions
+        self._required_actions = {}  # Dict of all required actions by definition ID
+        self._supported_actions = {}  # Dict of all supported actions by definition ID
+        self._first_start_actions = {}  # Dict of all actions that need to be done when first added by definition ID
+
+        # Add machine_action as plugin type
+        PluginRegistry.addType("machine_action", self.addMachineAction)
+
+        # Ensure that all containers that were registered before creation of this registry are also handled.
+        # This should not have any effect, but it makes it safer if we ever refactor the order of things.
+        for container in ContainerRegistry.getInstance().findDefinitionContainers():
+            self._onContainerAdded(container)
+
+        ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
+
+    def _onContainerAdded(self, container):
+        ## Ensure that the actions are added to this manager
+        if isinstance(container, DefinitionContainer):
+            supported_actions = container.getMetaDataEntry("supported_actions", [])
+            for action in supported_actions:
+                self.addSupportedAction(container.getId(), action)
+
+            required_actions = container.getMetaDataEntry("required_actions", [])
+            for action in required_actions:
+                self.addRequiredAction(container.getId(), action)
+
+            first_start_actions = container.getMetaDataEntry("first_start_actions", [])
+            for action in first_start_actions:
+                self.addFirstStartAction(container.getId(), action)
+
+    ##  Add a required action to a machine
+    #   Raises an exception when the action is not recognised.
+    def addRequiredAction(self, definition_id, action_key):
+        if action_key in self._machine_actions:
+            if definition_id in self._required_actions:
+                self._required_actions[definition_id] |= {self._machine_actions[action_key]}
+            else:
+                self._required_actions[definition_id] = {self._machine_actions[action_key]}
+        else:
+            raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
+
+    ##  Add a supported action to a machine.
+    def addSupportedAction(self, definition_id, action_key):
+        if action_key in self._machine_actions:
+            if definition_id in self._supported_actions:
+                self._supported_actions[definition_id] |= {self._machine_actions[action_key]}
+            else:
+                self._supported_actions[definition_id] = {self._machine_actions[action_key]}
+        else:
+            Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
+
+    ##  Add an action to the first start list of a machine.
+    def addFirstStartAction(self, definition_id, action_key, index = None):
+        if action_key in self._machine_actions:
+            if definition_id in self._first_start_actions:
+                if index is not None:
+                    self._first_start_actions[definition_id].insert(index, self._machine_actions[action_key])
+                else:
+                    self._first_start_actions[definition_id].append(self._machine_actions[action_key])
+            else:
+                self._first_start_actions[definition_id] = [self._machine_actions[action_key]]
+        else:
+            Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
+
+    ##  Add a (unique) MachineAction
+    #   if the Key of the action is not unique, an exception is raised.
+    def addMachineAction(self, action):
+        if action.getKey() not in self._machine_actions:
+            self._machine_actions[action.getKey()] = action
+        else:
+            raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey())
+
+    ##  Get all actions supported by given machine
+    #   \param definition_id The ID of the definition you want the supported actions of
+    #   \returns set of supported actions.
+    @pyqtSlot(str, result = "QVariantList")
+    def getSupportedActions(self, definition_id):
+        if definition_id in self._supported_actions:
+            return list(self._supported_actions[definition_id])
+        else:
+            return set()
+
+    ##  Get all actions required by given machine
+    #   \param definition_id The ID of the definition you want the required actions of
+    #   \returns set of required actions.
+    def getRequiredActions(self, definition_id):
+        if definition_id in self._required_actions:
+            return self._required_actions[definition_id]
+        else:
+            return set()
+
+    ##  Get all actions that need to be performed upon first start of a given machine.
+    #   Note that contrary to required / supported actions a list is returned (as it could be required to run the same
+    #   action multiple times).
+    #   \param definition_id The ID of the definition that you want to get the "on added" actions for.
+    #   \returns List of actions.
+    @pyqtSlot(str, result="QVariantList")
+    def getFirstStartActions(self, definition_id):
+        if definition_id in self._first_start_actions:
+            return self._first_start_actions[definition_id]
+        else:
+            return []
+
+    ##  Remove Machine action from manager
+    #   \param action to remove
+    def removeMachineAction(self, action):
+        try:
+            del self._machine_actions[action.getKey()]
+        except KeyError:
+            Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey())
+
+    ##  Get MachineAction by key
+    #   \param key String of key to select
+    #   \return Machine action if found, None otherwise
+    def getMachineAction(self, key):
+        if key in self._machine_actions:
+            return self._machine_actions[key]
+        else:
+            return None

+ 30 - 27
plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py

@@ -3,36 +3,44 @@ from UM.Application import Application
 from UM.Settings.SettingInstance import SettingInstance
 from UM.Logger import Logger
 
+import UM.Settings.Models
+
 from cura.SettingOverrideDecorator import SettingOverrideDecorator
 
 ##  The per object setting visibility handler ensures that only setting defintions that have a matching instance Container
 #   are returned as visible. 
-class PerObjectSettingVisibilityHandler(QObject):
+class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler):
     def __init__(self, parent = None, *args, **kwargs):
         super().__init__(parent = parent, *args, **kwargs)
-        self._selected_object_id = None
 
-    visibilityChanged = pyqtSignal()
+        self._selected_object_id = None
+        self._node = None
+        self._stack = None
 
     def setSelectedObjectId(self, id):
-        self._selected_object_id = id
-        self.visibilityChanged.emit()
+        if id != self._selected_object_id:
+            self._selected_object_id = id
+
+            self._node = Application.getInstance().getController().getScene().findObject(self._selected_object_id)
+            if self._node:
+                self._stack = self._node.callDecoration("getStack")
+
+            self.visibilityChanged.emit()
 
     @pyqtProperty("quint64", fset = setSelectedObjectId)
     def selectedObjectId(self):
-        pass
+        return self._selected_object_id
 
     def setVisible(self, visible):
-        node = Application.getInstance().getController().getScene().findObject(self._selected_object_id)
-        if not node:
+        if not self._node:
             return
-        stack = node.callDecoration("getStack")
-        if not stack:
-            node.addDecorator(SettingOverrideDecorator())
-            stack = node.callDecoration("getStack")
 
-        settings = stack.getTop()
-        all_instances = settings.findInstances(**{})
+        if not self._stack:
+            self._node.addDecorator(SettingOverrideDecorator())
+            self._stack = self._node.callDecoration("getStack")
+
+        settings = self._stack.getTop()
+        all_instances = settings.findInstances()
         visibility_changed = False  # Flag to check if at the end the signal needs to be emitted
 
         # Remove all instances that are not in visibility list
@@ -41,13 +49,12 @@ class PerObjectSettingVisibilityHandler(QObject):
                 settings.removeInstance(instance.definition.key)
                 visibility_changed = True
 
-        # Add all instances that are not added, but are in visiblity list
+        # Add all instances that are not added, but are in visibility list
         for item in visible:
             if not settings.getInstance(item):
-                definition_container = Application.getInstance().getGlobalContainerStack().getBottom()
-                definitions = definition_container.findDefinitions(key = item)
-                if definitions:
-                    settings.addInstance(SettingInstance(definitions[0], settings))
+                definition = self._stack.getSettingDefinition(item)
+                if definition:
+                    settings.addInstance(SettingInstance(definition, settings))
                     visibility_changed = True
                 else:
                     Logger.log("w", "Unable to add instance (%s) to perobject visibility because we couldn't find the matching definition", item)
@@ -57,20 +64,16 @@ class PerObjectSettingVisibilityHandler(QObject):
 
     def getVisible(self):
         visible_settings = set()
-        node = Application.getInstance().getController().getScene().findObject(self._selected_object_id)
-        if not node:
+        if not self._node:
             return visible_settings
 
-        stack = node.callDecoration("getStack")
-        if not stack:
+        if not self._stack:
             return visible_settings
 
-        settings = stack.getTop()
+        settings = self._stack.getTop()
         if not settings:
             return visible_settings
 
-        all_instances = settings.findInstances(**{})
-        for instance in all_instances:
-            visible_settings.add(instance.definition.key)
+        visible_settings = set(map(lambda i: i.definition.key, settings.findInstances()))
         return visible_settings
 

+ 5 - 2
plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml

@@ -153,6 +153,8 @@ Item {
             {
                 id: addedSettingsModel;
                 containerId: Cura.MachineManager.activeDefinitionId
+                expanded: [ "*" ]
+
                 visibilityHandler: Cura.PerObjectSettingVisibilityHandler
                 {
                     selectedObjectId: UM.ActiveTool.properties.getValue("SelectedObjectId")
@@ -215,9 +217,8 @@ Item {
 
                     style: ButtonStyle
                     {
-                        background: Rectangle
+                        background: Item
                         {
-                            color: control.hovered ? control.parent.style.controlHighlightColor : control.parent.style.controlColor;
                             UM.RecolorImage
                             {
                                 anchors.verticalCenter: parent.verticalCenter
@@ -340,6 +341,8 @@ Item {
                         "settable_per_mesh": true
                     }
                     visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
+                    expanded: [ "*" ]
+                    exclude: [ "machine_settings" ]
                 }
                 delegate:Loader
                 {

+ 53 - 0
plugins/UltimakerMachineActions/BedLevelMachineAction.py

@@ -0,0 +1,53 @@
+from cura.MachineAction import MachineAction
+
+from PyQt5.QtCore import  pyqtSlot
+
+from UM.Application import Application
+
+from cura.PrinterOutputDevice import PrinterOutputDevice
+
+class BedLevelMachineAction(MachineAction):
+    def __init__(self):
+        super().__init__("BedLevel", "Level bed")
+        self._qml_url = "BedLevelMachineAction.qml"
+        self._bed_level_position = 0
+
+    def _execute(self):
+        pass
+
+    def _reset(self):
+        self._bed_level_position = 0
+        printer_output_devices = self._getPrinterOutputDevices()
+        if printer_output_devices:
+            printer_output_devices[0].homeBed()
+            printer_output_devices[0].moveHead(0, 0, 3)
+            printer_output_devices[0].homeHead()
+
+    def _getPrinterOutputDevices(self):
+        return [printer_output_device for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices() if isinstance(printer_output_device, PrinterOutputDevice)]
+
+    @pyqtSlot()
+    def moveToNextLevelPosition(self):
+        output_devices = self._getPrinterOutputDevices()
+        if output_devices:  # We found at least one output device
+            output_device = output_devices[0]
+
+            if self._bed_level_position == 0:
+                output_device.moveHead(0, 0, 3)
+                output_device.homeHead()
+                output_device.moveHead(0, 0, 3)
+                output_device.moveHead(Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") - 10, 0, 0)
+                output_device.moveHead(0, 0, -3)
+                self._bed_level_position += 1
+            elif self._bed_level_position == 1:
+                output_device.moveHead(0, 0, 3)
+                output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value" ) / 2, Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") - 10, 0)
+                output_device.moveHead(0, 0, -3)
+                self._bed_level_position += 1
+            elif self._bed_level_position == 2:
+                output_device.moveHead(0, 0, 3)
+                output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") / 2 + 10, -(Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") + 10), 0)
+                output_device.moveHead(0, 0, -3)
+                self._bed_level_position += 1
+            elif self._bed_level_position >= 3:
+                self.setFinished()

+ 85 - 0
plugins/UltimakerMachineActions/BedLevelMachineAction.qml

@@ -0,0 +1,85 @@
+// Copyright (c) 2016 Ultimaker B.V.
+// Cura is released under the terms of the AGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 1.1
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.1
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+
+Cura.MachineAction
+{
+    anchors.fill: parent;
+    Item
+    {
+        id: bedLevelMachineAction
+        anchors.fill: parent;
+
+        UM.I18nCatalog { id: catalog; name: "cura"; }
+
+        Label
+        {
+            id: pageTitle
+            width: parent.width
+            text: catalog.i18nc("@title", "Bed Leveling")
+            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", "To make sure your prints will come out great, you can now adjust your buildplate. When you click 'Move to Next Position' the nozzle will move to the different positions that can be adjusted.")
+        }
+        Label
+        {
+            id: bedlevelingText
+            anchors.top: pageDescription.bottom
+            anchors.topMargin: UM.Theme.getSize("default_margin").height
+            width: parent.width
+            wrapMode: Text.WordWrap
+            text: catalog.i18nc("@label", "For every position; insert a piece of paper under the nozzle and adjust the print bed height. The print bed height is right when the paper is slightly gripped by the tip of the nozzle.")
+        }
+
+        Item
+        {
+            id: bedlevelingWrapper
+            anchors.top: bedlevelingText.bottom
+            anchors.topMargin: UM.Theme.getSize("default_margin").height
+            anchors.horizontalCenter: parent.horizontalCenter
+            height: skipBedlevelingButton.height
+            width: bedlevelingButton.width + skipBedlevelingButton.width + UM.Theme.getSize("default_margin").height < bedLevelMachineAction.width ? bedlevelingButton.width + skipBedlevelingButton.width + UM.Theme.getSize("default_margin").height : bedLevelMachineAction.width
+            Button
+            {
+                id: bedlevelingButton
+                anchors.top: parent.top
+                anchors.left: parent.left
+                text: catalog.i18nc("@action:button","Move to Next Position");
+                onClicked:
+                {
+                    manager.moveToNextLevelPosition()
+                }
+            }
+
+            Button
+            {
+                id: skipBedlevelingButton
+                anchors.top: parent.width < bedLevelMachineAction.width ? parent.top : bedlevelingButton.bottom
+                anchors.topMargin: parent.width < bedLevelMachineAction.width ? 0 : UM.Theme.getSize("default_margin").height/2
+                anchors.left: parent.width < bedLevelMachineAction.width ? bedlevelingButton.right : parent.left
+                anchors.leftMargin: parent.width < bedLevelMachineAction.width ? UM.Theme.getSize("default_margin").width : 0
+                text: catalog.i18nc("@action:button","Skip bed leveling");
+                onClicked:
+                {
+                    manager.setFinished()
+                }
+            }
+        }
+    }
+}

+ 160 - 0
plugins/UltimakerMachineActions/UMOCheckupMachineAction.py

@@ -0,0 +1,160 @@
+from cura.MachineAction import MachineAction
+from cura.PrinterOutputDevice import PrinterOutputDevice
+from UM.Application import Application
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
+
+class UMOCheckupMachineAction(MachineAction):
+    def __init__(self):
+        super().__init__("UMOCheckup", "Checkup")
+        self._qml_url = "UMOCheckupMachineAction.qml"
+        self._hotend_target_temp = 180
+        self._bed_target_temp = 60
+        self._output_device = None
+        self._bed_test_completed = False
+        self._hotend_test_completed = False
+
+        # Endstop tests
+        self._x_min_endstop_test_completed = False
+        self._y_min_endstop_test_completed = False
+        self._z_min_endstop_test_completed = False
+
+        self._check_started = False
+
+        Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
+
+
+    onBedTestCompleted = pyqtSignal()
+    onHotendTestCompleted = pyqtSignal()
+
+    onXMinEndstopTestCompleted = pyqtSignal()
+    onYMinEndstopTestCompleted = pyqtSignal()
+    onZMinEndstopTestCompleted = pyqtSignal()
+
+    bedTemperatureChanged = pyqtSignal()
+    hotendTemperatureChanged = pyqtSignal()
+
+    def _onOutputDevicesChanged(self):
+        # Check if this action was started, but no output device was found the first time.
+        # If so, re-try now that an output device has been added/removed.
+        if self._output_device is None and self._check_started:
+            self.startCheck()
+
+
+    def _getPrinterOutputDevices(self):
+        return [printer_output_device for printer_output_device in
+                Application.getInstance().getOutputDeviceManager().getOutputDevices() if
+                isinstance(printer_output_device, PrinterOutputDevice)]
+
+    def _reset(self):
+        if self._output_device:
+            self._output_device.bedTemperatureChanged.disconnect(self.bedTemperatureChanged)
+            self._output_device.hotendTemperaturesChanged.disconnect(self.hotendTemperatureChanged)
+            self._output_device.bedTemperatureChanged.disconnect(self._onBedTemperatureChanged)
+            self._output_device.hotendTemperaturesChanged.disconnect(self._onHotendTemperatureChanged)
+            self._output_device.endstopStateChanged.disconnect(self._onEndstopStateChanged)
+            try:
+                self._output_device.stopPollEndstop()
+            except AttributeError:  # Connection is probably not a USB connection. Something went pretty wrong if this happens.
+                pass
+        self._output_device = None
+
+        self._check_started = False
+
+        # Ensure everything is reset (and right signals are emitted again)
+        self._bed_test_completed = False
+        self.onBedTestCompleted.emit()
+        self._hotend_test_completed = False
+        self.onHotendTestCompleted.emit()
+
+        self._x_min_endstop_test_completed = False
+        self.onXMinEndstopTestCompleted.emit()
+        self._y_min_endstop_test_completed = False
+        self.onYMinEndstopTestCompleted.emit()
+        self._z_min_endstop_test_completed = False
+        self.onZMinEndstopTestCompleted.emit()
+
+    @pyqtProperty(bool, notify = onBedTestCompleted)
+    def bedTestCompleted(self):
+        return self._bed_test_completed
+
+    @pyqtProperty(bool, notify = onHotendTestCompleted)
+    def hotendTestCompleted(self):
+        return self._hotend_test_completed
+
+    @pyqtProperty(bool, notify = onXMinEndstopTestCompleted)
+    def xMinEndstopTestCompleted(self):
+        return self._x_min_endstop_test_completed
+
+    @pyqtProperty(bool, notify=onYMinEndstopTestCompleted)
+    def yMinEndstopTestCompleted(self):
+        return self._y_min_endstop_test_completed
+
+    @pyqtProperty(bool, notify=onZMinEndstopTestCompleted)
+    def zMinEndstopTestCompleted(self):
+        return self._z_min_endstop_test_completed
+
+    @pyqtProperty(float, notify = bedTemperatureChanged)
+    def bedTemperature(self):
+        if not self._output_device:
+            return 0
+        return self._output_device.bedTemperature
+
+    @pyqtProperty(float, notify=hotendTemperatureChanged)
+    def hotendTemperature(self):
+        if not self._output_device:
+            return 0
+        return self._output_device.hotendTemperatures[0]
+
+    def _onHotendTemperatureChanged(self):
+        if not self._output_device:
+            return
+        if not self._hotend_test_completed:
+            if self._output_device.hotendTemperatures[0] + 10 > self._hotend_target_temp and self._output_device.hotendTemperatures[0] - 10 < self._hotend_target_temp:
+                self._hotend_test_completed = True
+                self.onHotendTestCompleted.emit()
+
+    def _onBedTemperatureChanged(self):
+        if not self._output_device:
+            return
+        if not self._bed_test_completed:
+            if self._output_device.bedTemperature + 5 > self._bed_target_temp and self._output_device.bedTemperature - 5 < self._bed_target_temp:
+                self._bed_test_completed = True
+                self.onBedTestCompleted.emit()
+
+    def _onEndstopStateChanged(self, switch_type, state):
+        if state:
+            if switch_type == "x_min":
+                self._x_min_endstop_test_completed = True
+                self.onXMinEndstopTestCompleted.emit()
+            elif switch_type == "y_min":
+                self._y_min_endstop_test_completed = True
+                self.onYMinEndstopTestCompleted.emit()
+            elif switch_type == "z_min":
+                self._z_min_endstop_test_completed = True
+                self.onZMinEndstopTestCompleted.emit()
+
+    @pyqtSlot()
+    def startCheck(self):
+        self._check_started = True
+        output_devices = self._getPrinterOutputDevices()
+        if output_devices:
+            self._output_device = output_devices[0]
+            try:
+                self._output_device.startPollEndstop()
+                self._output_device.bedTemperatureChanged.connect(self.bedTemperatureChanged)
+                self._output_device.hotendTemperaturesChanged.connect(self.hotendTemperatureChanged)
+                self._output_device.bedTemperatureChanged.connect(self._onBedTemperatureChanged)
+                self._output_device.hotendTemperaturesChanged.connect(self._onHotendTemperatureChanged)
+                self._output_device.endstopStateChanged.connect(self._onEndstopStateChanged)
+            except AttributeError:  # Connection is probably not a USB connection. Something went pretty wrong if this happens.
+                pass
+
+    @pyqtSlot()
+    def heatupHotend(self):
+        if self._output_device is not None:
+            self._output_device.setTargetHotendTemperature(0, self._hotend_target_temp)
+
+    @pyqtSlot()
+    def heatupBed(self):
+        if self._output_device is not None:
+            self._output_device.setTargetBedTemperature(self._bed_target_temp)

+ 271 - 0
plugins/UltimakerMachineActions/UMOCheckupMachineAction.qml

@@ -0,0 +1,271 @@
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+import QtQuick 2.2
+import QtQuick.Controls 1.1
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.1
+
+Cura.MachineAction
+{
+    anchors.fill: parent;
+    Item
+    {
+        id: checkupMachineAction
+        anchors.fill: parent;
+        property int leftRow: checkupMachineAction.width * 0.40
+        property int rightRow: checkupMachineAction.width * 0.60
+        UM.I18nCatalog { id: catalog; name:"cura"}
+        Label
+        {
+            id: pageTitle
+            width: parent.width
+            text: catalog.i18nc("@title", "Check Printer")
+            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","It's a good idea to do a few sanity checks on your Ultimaker. You can skip this step if you know your machine is functional");
+        }
+
+        Item
+        {
+            id: startStopButtons
+            anchors.top: pageDescription.bottom
+            anchors.topMargin: UM.Theme.getSize("default_margin").height
+            anchors.horizontalCenter: parent.horizontalCenter
+            height: childrenRect.height
+            width: startCheckButton.width + skipCheckButton.width + UM.Theme.getSize("default_margin").height < checkupMachineAction.width ? startCheckButton.width + skipCheckButton.width + UM.Theme.getSize("default_margin").height : checkupMachineAction.width
+            Button
+            {
+                id: startCheckButton
+                anchors.top: parent.top
+                anchors.left: parent.left
+                text: catalog.i18nc("@action:button","Start Printer Check");
+                onClicked:
+                {
+                    checkupContent.visible = true
+                    startCheckButton.enabled = false
+                    manager.startCheck()
+                }
+            }
+
+            Button
+            {
+                id: skipCheckButton
+                anchors.top: parent.width < checkupMachineAction.width ? parent.top : startCheckButton.bottom
+                anchors.topMargin: parent.width < checkupMachineAction.width ? 0 : UM.Theme.getSize("default_margin").height/2
+                anchors.left: parent.width < checkupMachineAction.width ? startCheckButton.right : parent.left
+                anchors.leftMargin: parent.width < checkupMachineAction.width ? UM.Theme.getSize("default_margin").width : 0
+                text: catalog.i18nc("@action:button", "Skip Printer Check");
+                onClicked: manager.setFinished()
+            }
+        }
+
+        Item
+        {
+            id: checkupContent
+            anchors.top: startStopButtons.bottom
+            anchors.topMargin: UM.Theme.getSize("default_margin").height
+            visible: false
+            width: parent.width
+            height: 250
+            //////////////////////////////////////////////////////////
+            Label
+            {
+                id: connectionLabel
+                width: checkupMachineAction.leftRow
+                anchors.left: parent.left
+                anchors.top: parent.top
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@label","Connection: ")
+            }
+            Label
+            {
+                id: connectionStatus
+                width: checkupMachineAction.rightRow
+                anchors.left: connectionLabel.right
+                anchors.top: parent.top
+                wrapMode: Text.WordWrap
+                text: Cura.USBPrinterManager.connectedPrinterList.rowCount() > 0 || base.addOriginalProgress.checkUp[0] ? catalog.i18nc("@info:status","Done"):catalog.i18nc("@info:status","Incomplete")
+            }
+            //////////////////////////////////////////////////////////
+            Label
+            {
+                id: endstopXLabel
+                width: checkupMachineAction.leftRow
+                anchors.left: parent.left
+                anchors.top: connectionLabel.bottom
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@label","Min endstop X: ")
+            }
+            Label
+            {
+                id: endstopXStatus
+                width: checkupMachineAction.rightRow
+                anchors.left: endstopXLabel.right
+                anchors.top: connectionLabel.bottom
+                wrapMode: Text.WordWrap
+                text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
+            }
+            //////////////////////////////////////////////////////////////
+            Label
+            {
+                id: endstopYLabel
+                width: checkupMachineAction.leftRow
+                anchors.left: parent.left
+                anchors.top: endstopXLabel.bottom
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@label","Min endstop Y: ")
+            }
+            Label
+            {
+                id: endstopYStatus
+                width: checkupMachineAction.rightRow
+                anchors.left: endstopYLabel.right
+                anchors.top: endstopXLabel.bottom
+                wrapMode: Text.WordWrap
+                text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
+            }
+            /////////////////////////////////////////////////////////////////////
+            Label
+            {
+                id: endstopZLabel
+                width: checkupMachineAction.leftRow
+                anchors.left: parent.left
+                anchors.top: endstopYLabel.bottom
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@label","Min endstop Z: ")
+            }
+            Label
+            {
+                id: endstopZStatus
+                width: checkupMachineAction.rightRow
+                anchors.left: endstopZLabel.right
+                anchors.top: endstopYLabel.bottom
+                wrapMode: Text.WordWrap
+                text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
+            }
+            ////////////////////////////////////////////////////////////
+            Label
+            {
+                id: nozzleTempLabel
+                width: checkupMachineAction.leftRow
+                anchors.left: parent.left
+                anchors.top: endstopZLabel.bottom
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@label","Nozzle temperature check: ")
+            }
+            Label
+            {
+                id: nozzleTempStatus
+                width: checkupMachineAction.rightRow * 0.4
+                anchors.top: nozzleTempLabel.top
+                anchors.left: nozzleTempLabel.right
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@info:status","Not checked")
+            }
+            Item
+            {
+                id: nozzleTempButton
+                width: checkupMachineAction.rightRow * 0.3
+                height: nozzleTemp.height
+                anchors.top: nozzleTempLabel.top
+                anchors.left: bedTempStatus.right
+                anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
+                Button
+                {
+                    height: nozzleTemp.height - 2
+                    anchors.verticalCenter: parent.verticalCenter
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    text: catalog.i18nc("@action:button","Start Heating")
+                    onClicked:
+                    {
+                        manager.heatupHotend()
+                        nozzleTempStatus.text = catalog.i18nc("@info:progress","Checking")
+                    }
+                }
+            }
+            Label
+            {
+                id: nozzleTemp
+                anchors.top: nozzleTempLabel.top
+                anchors.left: nozzleTempButton.right
+                anchors.leftMargin: UM.Theme.getSize("default_margin").width
+                width: checkupMachineAction.rightRow * 0.2
+                wrapMode: Text.WordWrap
+                text: manager.hotendTemperature + "°C"
+                font.bold: true
+            }
+            /////////////////////////////////////////////////////////////////////////////
+            Label
+            {
+                id: bedTempLabel
+                width: checkupMachineAction.leftRow
+                anchors.left: parent.left
+                anchors.top: nozzleTempLabel.bottom
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@label","bed temperature check:")
+            }
+
+            Label
+            {
+                id: bedTempStatus
+                width: checkupMachineAction.rightRow * 0.4
+                anchors.top: bedTempLabel.top
+                anchors.left: bedTempLabel.right
+                wrapMode: Text.WordWrap
+                text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked")
+            }
+            Item
+            {
+                id: bedTempButton
+                width: checkupMachineAction.rightRow * 0.3
+                height: bedTemp.height
+                anchors.top: bedTempLabel.top
+                anchors.left: bedTempStatus.right
+                anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
+                Button
+                {
+                    height: bedTemp.height - 2
+                    anchors.verticalCenter: parent.verticalCenter
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    text: catalog.i18nc("@action:button","Start Heating")
+                    onClicked:
+                    {
+                        manager.heatupBed()
+                    }
+                }
+            }
+            Label
+            {
+                id: bedTemp
+                width: checkupMachineAction.rightRow * 0.2
+                anchors.top: bedTempLabel.top
+                anchors.left: bedTempButton.right
+                anchors.leftMargin: UM.Theme.getSize("default_margin").width
+                wrapMode: Text.WordWrap
+                text: manager.bedTemperature + "°C"
+                font.bold: true
+            }
+            Label
+            {
+                id: resultText
+                visible: false
+                anchors.top: bedTemp.bottom
+                anchors.topMargin: UM.Theme.getSize("default_margin").height
+                anchors.left: parent.left
+                width: parent.width
+                wrapMode: Text.WordWrap
+                text: catalog.i18nc("@label", "Everything is in order! You're done with your CheckUp.")
+            }
+        }
+    }
+}

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