MachineErrorChecker.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import time
  4. from collections import deque
  5. from PyQt6.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
  6. from typing import Optional, Any, Set
  7. from UM.Logger import Logger
  8. from UM.Settings.SettingDefinition import SettingDefinition
  9. from UM.Settings.Validator import ValidatorState
  10. import cura.CuraApplication
  11. class MachineErrorChecker(QObject):
  12. """This class performs setting error checks for the currently active machine.
  13. The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. The idea
  14. here is to split the whole error check into small tasks, each of which only checks a single setting key in a
  15. stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should be
  16. good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
  17. for it to finish the complete work.
  18. """
  19. def __init__(self, parent: Optional[QObject] = None) -> None:
  20. super().__init__(parent)
  21. self._global_stack = None
  22. self._has_errors = True # Result of the error check, indicating whether there are errors in the stack
  23. self._error_keys = set() # type: Set[str] # A set of settings keys that have errors
  24. self._error_keys_in_progress = set() # type: Set[str] # The variable that stores the results of the currently in progress check
  25. self._stacks_and_keys_to_check = None # type: Optional[deque] # a FIFO queue of tuples (stack, key) to check for errors
  26. self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new
  27. # error check needs to take place while there is already one running at the moment.
  28. self._check_in_progress = False # Whether there is an error check running in progress at the moment.
  29. self._application = cura.CuraApplication.CuraApplication.getInstance()
  30. self._machine_manager = self._application.getMachineManager()
  31. self._check_start_time = time.time()
  32. self._setCheckTimer()
  33. self._keys_to_check = set() # type: Set[str]
  34. self._num_keys_to_check_per_update = 1
  35. def initialize(self) -> None:
  36. self._error_check_timer.timeout.connect(self._rescheduleCheck)
  37. # Reconnect all signals when the active machine gets changed.
  38. self._machine_manager.globalContainerChanged.connect(self._onMachineChanged)
  39. # Whenever the machine settings get changed, we schedule an error check.
  40. self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)
  41. self._onMachineChanged()
  42. def _setCheckTimer(self) -> None:
  43. """A QTimer to regulate error check frequency
  44. This timer delays the starting of error check
  45. so we can react less frequently if the user is frequently
  46. changing settings.
  47. """
  48. self._error_check_timer = QTimer(self)
  49. self._error_check_timer.setInterval(100)
  50. self._error_check_timer.setSingleShot(True)
  51. def _onMachineChanged(self) -> None:
  52. if self._global_stack:
  53. self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
  54. self._global_stack.containersChanged.disconnect(self.startErrorCheck)
  55. for extruder in self._global_stack.extruderList:
  56. extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
  57. extruder.containersChanged.disconnect(self.startErrorCheck)
  58. self._global_stack = self._machine_manager.activeMachine
  59. if self._global_stack:
  60. self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
  61. self._global_stack.containersChanged.connect(self.startErrorCheck)
  62. for extruder in self._global_stack.extruderList:
  63. extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
  64. extruder.containersChanged.connect(self.startErrorCheck)
  65. hasErrorUpdated = pyqtSignal()
  66. needToWaitForResultChanged = pyqtSignal()
  67. errorCheckFinished = pyqtSignal()
  68. @pyqtProperty(bool, notify = hasErrorUpdated)
  69. def hasError(self) -> bool:
  70. return self._has_errors
  71. @pyqtProperty(bool, notify = needToWaitForResultChanged)
  72. def needToWaitForResult(self) -> bool:
  73. return self._need_to_check or self._check_in_progress
  74. def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None:
  75. """Start the error check for property changed
  76. this is separate from the startErrorCheck because it ignores a number property types
  77. :param key:
  78. :param property_name:
  79. """
  80. if property_name != "value":
  81. return
  82. self._keys_to_check.add(key)
  83. self.startErrorCheck()
  84. def startErrorCheck(self, *args: Any) -> None:
  85. """Starts the error check timer to schedule a new error check.
  86. :param args:
  87. """
  88. if not self._check_in_progress:
  89. self._need_to_check = True
  90. self.needToWaitForResultChanged.emit()
  91. self._error_check_timer.start()
  92. def _rescheduleCheck(self) -> None:
  93. """This function is called by the timer to reschedule a new error check.
  94. If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
  95. to notify the current check to stop and start a new one.
  96. """
  97. if self._check_in_progress and not self._need_to_check:
  98. self._need_to_check = True
  99. self.needToWaitForResultChanged.emit()
  100. return
  101. self._error_keys_in_progress = set()
  102. self._need_to_check = False
  103. self.needToWaitForResultChanged.emit()
  104. global_stack = self._machine_manager.activeMachine
  105. if global_stack is None:
  106. Logger.log("i", "No active machine, nothing to check.")
  107. return
  108. # Populate the (stack, key) tuples to check
  109. self._stacks_and_keys_to_check = deque()
  110. for stack in global_stack.extruderList:
  111. if not self._keys_to_check:
  112. self._keys_to_check = stack.getAllKeys()
  113. for key in self._keys_to_check:
  114. self._stacks_and_keys_to_check.append((stack, key))
  115. self._application.callLater(self._checkStack)
  116. self._check_start_time = time.time()
  117. Logger.log("d", "New error check scheduled.")
  118. def _checkStack(self) -> None:
  119. if self._need_to_check:
  120. Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
  121. self._check_in_progress = False
  122. self._application.callLater(self.startErrorCheck)
  123. return
  124. self._check_in_progress = True
  125. for i in range(self._num_keys_to_check_per_update):
  126. # If there is nothing to check any more, it means there is no error.
  127. if not self._stacks_and_keys_to_check:
  128. # Finish
  129. self._setResult(False)
  130. return
  131. # Get the next stack and key to check
  132. stack, key = self._stacks_and_keys_to_check.popleft()
  133. enabled = stack.getProperty(key, "enabled")
  134. if not enabled:
  135. continue
  136. validation_state = stack.getProperty(key, "validationState")
  137. if validation_state is None:
  138. # Setting is not validated. This can happen if there is only a setting definition.
  139. # We do need to validate it, because a setting definitions value can be set by a function, which could
  140. # be an invalid setting.
  141. definition = stack.getSettingDefinition(key)
  142. validator_type = SettingDefinition.getValidatorForType(definition.type)
  143. if validator_type:
  144. validator = validator_type(key)
  145. validation_state = validator(stack)
  146. if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
  147. # Since we don't know if any of the settings we didn't check is has an error value, store the list for the
  148. # next check.
  149. keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check}
  150. keys_to_recheck.add(key)
  151. self._setResult(True, keys_to_recheck = keys_to_recheck)
  152. return
  153. # Schedule the check for the next key
  154. self._application.callLater(self._checkStack)
  155. def _setResult(self, result: bool, keys_to_recheck = None) -> None:
  156. if result != self._has_errors:
  157. self._has_errors = result
  158. self.hasErrorUpdated.emit()
  159. self._machine_manager.stacksValidationChanged.emit()
  160. self._keys_to_check = keys_to_recheck if keys_to_recheck else set()
  161. self._need_to_check = False
  162. self._check_in_progress = False
  163. self.needToWaitForResultChanged.emit()
  164. self.errorCheckFinished.emit()
  165. execution_time = time.time() - self._check_start_time
  166. Logger.info(f"Error check finished, result = {result}, time = {execution_time:.2f}s")