Welcome to mirror list, hosted at ThFree Co, Russian Federation.

MachineErrorChecker.py « Machines « cura - github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 58ca8e49efc9b09261caed2f970eb49e99f50986 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

import time

from collections import deque

from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
from typing import Optional, Any, Set

from UM.Logger import Logger
from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.Validator import ValidatorState

import cura.CuraApplication


class MachineErrorChecker(QObject):
    """This class performs setting error checks for the currently active machine.

    The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. The idea
    here is to split the whole error check into small tasks, each of which only checks a single setting key in a
    stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should be
    good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
    for it to finish the complete work.
    """

    def __init__(self, parent: Optional[QObject] = None) -> None:
        super().__init__(parent)

        self._global_stack = None

        self._has_errors = True  # Result of the error check, indicating whether there are errors in the stack
        self._error_keys = set()  # type: Set[str] # A set of settings keys that have errors
        self._error_keys_in_progress = set()  # type: Set[str]  # The variable that stores the results of the currently in progress check

        self._stacks_and_keys_to_check = None  # type: Optional[deque]  # a FIFO queue of tuples (stack, key) to check for errors

        self._need_to_check = False  # Whether we need to schedule a new check or not. This flag is set when a new
                                     # error check needs to take place while there is already one running at the moment.
        self._check_in_progress = False  # Whether there is an error check running in progress at the moment.

        self._application = cura.CuraApplication.CuraApplication.getInstance()
        self._machine_manager = self._application.getMachineManager()

        self._start_time = 0.  # measure checking time

        # This timer delays the starting of error check so we can react less frequently if the user is frequently
        # changing settings.
        self._error_check_timer = QTimer(self)
        self._error_check_timer.setInterval(100)
        self._error_check_timer.setSingleShot(True)
        self._error_check_timer.timeout.connect(self._rescheduleCheck)

        self._keys_to_check = set()  # type: Set[str]

        # Reconnect all signals when the active machine gets changed.
        self._machine_manager.globalContainerChanged.connect(self._onMachineChanged)

        # Whenever the machine settings get changed, we schedule an error check.
        self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)

        self._onMachineChanged()

    def _onMachineChanged(self) -> None:
        if self._global_stack:
            self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
            self._global_stack.containersChanged.disconnect(self.startErrorCheck)

            for extruder in self._global_stack.extruderList:
                extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
                extruder.containersChanged.disconnect(self.startErrorCheck)

        self._global_stack = self._machine_manager.activeMachine

        if self._global_stack:
            self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
            self._global_stack.containersChanged.connect(self.startErrorCheck)

            for extruder in self._global_stack.extruderList:
                extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
                extruder.containersChanged.connect(self.startErrorCheck)

    hasErrorUpdated = pyqtSignal()
    needToWaitForResultChanged = pyqtSignal()
    errorCheckFinished = pyqtSignal()

    @pyqtProperty(bool, notify = hasErrorUpdated)
    def hasError(self) -> bool:
        return self._has_errors

    @pyqtProperty(bool, notify = needToWaitForResultChanged)
    def needToWaitForResult(self) -> bool:
        return self._need_to_check or self._check_in_progress

    def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None:
        """Start the error check for property changed
        this is separate from the startErrorCheck because it ignores a number property types

        :param key:
        :param property_name:
        """

        if property_name != "value":
            return
        self._keys_to_check.add(key)
        self.startErrorCheck()

    def startErrorCheck(self, *args: Any) -> None:
        """Starts the error check timer to schedule a new error check.

        :param args:
        """

        if not self._check_in_progress:
            self._need_to_check = True
            self.needToWaitForResultChanged.emit()
        self._error_check_timer.start()

    def _rescheduleCheck(self) -> None:
        """This function is called by the timer to reschedule a new error check.

        If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
        to notify the current check to stop and start a new one.
        """

        if self._check_in_progress and not self._need_to_check:
            self._need_to_check = True
            self.needToWaitForResultChanged.emit()
            return

        self._error_keys_in_progress = set()
        self._need_to_check = False
        self.needToWaitForResultChanged.emit()

        global_stack = self._machine_manager.activeMachine
        if global_stack is None:
            Logger.log("i", "No active machine, nothing to check.")
            return

        # Populate the (stack, key) tuples to check
        self._stacks_and_keys_to_check = deque()
        for stack in global_stack.extruderList:
            if not self._keys_to_check:
                self._keys_to_check = stack.getAllKeys()

            for key in self._keys_to_check:
                self._stacks_and_keys_to_check.append((stack, key))

        self._application.callLater(self._checkStack)
        self._start_time = time.time()
        Logger.log("d", "New error check scheduled.")

    def _checkStack(self) -> None:
        if self._need_to_check:
            Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
            self._check_in_progress = False
            self._application.callLater(self.startErrorCheck)
            return

        self._check_in_progress = True

        # If there is nothing to check any more, it means there is no error.
        if not self._stacks_and_keys_to_check:
            # Finish
            self._setResult(False)
            return

        # Get the next stack and key to check
        stack, key = self._stacks_and_keys_to_check.popleft()

        enabled = stack.getProperty(key, "enabled")
        if not enabled:
            self._application.callLater(self._checkStack)
            return

        validation_state = stack.getProperty(key, "validationState")
        if validation_state is None:
            # Setting is not validated. This can happen if there is only a setting definition.
            # We do need to validate it, because a setting definitions value can be set by a function, which could
            # be an invalid setting.
            definition = stack.getSettingDefinition(key)
            validator_type = SettingDefinition.getValidatorForType(definition.type)
            if validator_type:
                validator = validator_type(key)
                validation_state = validator(stack)
        if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
            # Since we don't know if any of the settings we didn't check is has an error value, store the list for the
            # next check.
            keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check}
            keys_to_recheck.add(key)
            self._setResult(True, keys_to_recheck = keys_to_recheck)
            return

        # Schedule the check for the next key
        self._application.callLater(self._checkStack)

    def _setResult(self, result: bool, keys_to_recheck = None) -> None:
        if result != self._has_errors:
            self._has_errors = result
            self.hasErrorUpdated.emit()
            self._machine_manager.stacksValidationChanged.emit()
        if keys_to_recheck is None:
            self._keys_to_check = set()
        else:
            self._keys_to_check = keys_to_recheck
        self._need_to_check = False
        self._check_in_progress = False
        self.needToWaitForResultChanged.emit()
        self.errorCheckFinished.emit()
        Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time)