MachineErrorChecker.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from collections import deque
  4. from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
  5. from UM.Application import Application
  6. from UM.Logger import Logger
  7. #
  8. # This class performs setting error checks for the currently active machine.
  9. #
  10. # The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag.
  11. # The idea here is to split the whole error check into small tasks, each of which only checks a single setting key
  12. # in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should
  13. # be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
  14. # for it to finish the complete work.
  15. #
  16. class MachineErrorChecker(QObject):
  17. def __init__(self, parent = None):
  18. super().__init__(parent)
  19. self._global_stack = None
  20. self._has_errors = True # Result of the error check, indicating whether there are errors in the stack
  21. self._error_keys = set() # A set of settings keys that have errors
  22. self._error_keys_in_progress = set() # The variable that stores the results of the currently in progress check
  23. self._stacks_to_check = None # a FIFO queue of stacks to check for errors
  24. self._keys_to_check = None # a FIFO queue of setting keys to check for errors
  25. self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new
  26. # error check needs to take place while there is already one running at the moment.
  27. self._check_in_progress = False # Whether there is an error check running in progress at the moment.
  28. self._application = Application.getInstance()
  29. self._machine_manager = self._application.getMachineManager()
  30. # This timer delays the starting of error check so we can react less frequently if the user is frequently
  31. # changing settings.
  32. self._error_check_timer = QTimer(self)
  33. self._error_check_timer.setInterval(300)
  34. self._error_check_timer.setSingleShot(True)
  35. def initialize(self):
  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._machine_manager.globalValueChanged.connect(self.startErrorCheck)
  42. self._onMachineChanged()
  43. def _onMachineChanged(self):
  44. if self._global_stack:
  45. self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
  46. self._global_stack.containersChanged.disconnect(self.startErrorCheck)
  47. for extruder in self._global_stack.extruders.values():
  48. extruder.propertyChanged.disconnect(self.startErrorCheck)
  49. extruder.containersChanged.disconnect(self.startErrorCheck)
  50. self._global_stack = self._machine_manager.activeMachine
  51. if self._global_stack:
  52. self._global_stack.propertyChanged.connect(self.startErrorCheck)
  53. self._global_stack.containersChanged.connect(self.startErrorCheck)
  54. for extruder in self._global_stack.extruders.values():
  55. extruder.propertyChanged.connect(self.startErrorCheck)
  56. extruder.containersChanged.connect(self.startErrorCheck)
  57. hasErrorUpdated = pyqtSignal()
  58. needToWaitForResultChanged = pyqtSignal()
  59. errorCheckFinished = pyqtSignal()
  60. @pyqtProperty(bool, notify = hasErrorUpdated)
  61. def hasError(self) -> bool:
  62. return self._has_errors
  63. @pyqtProperty(bool, notify = needToWaitForResultChanged)
  64. def needToWaitForResult(self) -> bool:
  65. return self._need_to_check or self._check_in_progress
  66. # Starts the error check timer to schedule a new error check.
  67. def startErrorCheck(self, *args):
  68. if not self._check_in_progress:
  69. self._need_to_check = True
  70. self.needToWaitForResultChanged.emit()
  71. self._error_check_timer.start()
  72. # This function is called by the timer to reschedule a new error check.
  73. # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
  74. # to notify the current check to stop and start a new one.
  75. def _rescheduleCheck(self):
  76. if self._check_in_progress and not self._need_to_check:
  77. self._need_to_check = True
  78. self.needToWaitForResultChanged.emit()
  79. return
  80. self._error_keys_in_progress = set()
  81. self._need_to_check = False
  82. self.needToWaitForResultChanged.emit()
  83. global_stack = self._machine_manager.activeMachine
  84. if global_stack is None:
  85. Logger.log("i", "No active machine, nothing to check.")
  86. return
  87. self._stacks_to_check = deque([global_stack] + list(global_stack.extruders.values()))
  88. self._keys_to_check = deque(global_stack.getAllKeys())
  89. self._application.callLater(self._checkStack)
  90. Logger.log("d", "New error check scheduled.")
  91. def _checkStack(self):
  92. from UM.Settings.SettingDefinition import SettingDefinition
  93. from UM.Settings.Validator import ValidatorState
  94. if self._need_to_check:
  95. Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
  96. self._check_in_progress = False
  97. self._application.callLater(self.startErrorCheck)
  98. return
  99. self._check_in_progress = True
  100. # If there is nothing to check any more, it means there is no error.
  101. if not self._stacks_to_check or not self._keys_to_check:
  102. # Finish
  103. self._setResult(False)
  104. return
  105. stack = self._stacks_to_check[0]
  106. key = self._keys_to_check.popleft()
  107. # If there is no key left in this stack, check the next stack later.
  108. if not self._keys_to_check:
  109. if len(self._stacks_to_check) == 1:
  110. stacks = None
  111. keys = None
  112. else:
  113. stack = self._stacks_to_check.popleft()
  114. self._keys_to_check = deque(stack.getAllKeys())
  115. enabled = stack.getProperty(key, "enabled")
  116. if not enabled:
  117. self._application.callLater(self._checkStack)
  118. return
  119. validation_state = stack.getProperty(key, "validationState")
  120. if validation_state is None:
  121. # Setting is not validated. This can happen if there is only a setting definition.
  122. # We do need to validate it, because a setting definitions value can be set by a function, which could
  123. # be an invalid setting.
  124. definition = stack.getSettingDefinition(key)
  125. validator_type = SettingDefinition.getValidatorForType(definition.type)
  126. if validator_type:
  127. validator = validator_type(key)
  128. validation_state = validator(stack)
  129. if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
  130. # Finish
  131. self._setResult(True)
  132. return
  133. # Schedule the check for the next key
  134. self._application.callLater(self._checkStack)
  135. def _setResult(self, result: bool):
  136. if result != self._has_errors:
  137. self._has_errors = result
  138. self.hasErrorUpdated.emit()
  139. self._machine_manager.stacksValidationChanged.emit()
  140. self._need_to_check = False
  141. self._check_in_progress = False
  142. self.needToWaitForResultChanged.emit()
  143. self.errorCheckFinished.emit()
  144. Logger.log("i", "Error check finished, result = %s", result)