GlobalStack.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from collections import defaultdict
  4. import threading
  5. from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
  6. from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
  7. from UM.Decorators import override
  8. from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
  9. from UM.Settings.ContainerStack import ContainerStack
  10. from UM.Settings.SettingInstance import InstanceState
  11. from UM.Settings.ContainerRegistry import ContainerRegistry
  12. from UM.Settings.Interfaces import PropertyEvaluationContext
  13. from UM.Logger import Logger
  14. from UM.Resources import Resources
  15. from UM.Platform import Platform
  16. from UM.Util import parseBool
  17. import cura.CuraApplication
  18. from . import Exceptions
  19. from .CuraContainerStack import CuraContainerStack
  20. if TYPE_CHECKING:
  21. from cura.Settings.ExtruderStack import ExtruderStack
  22. ## Represents the Global or Machine stack and its related containers.
  23. #
  24. class GlobalStack(CuraContainerStack):
  25. def __init__(self, container_id: str) -> None:
  26. super().__init__(container_id)
  27. self.setMetaDataEntry("type", "machine") # For backward compatibility
  28. self._extruders = {} # type: Dict[str, "ExtruderStack"]
  29. # This property is used to track which settings we are calculating the "resolve" for
  30. # and if so, to bypass the resolve to prevent an infinite recursion that would occur
  31. # if the resolve function tried to access the same property it is a resolve for.
  32. # Per thread we have our own resolving_settings, or strange things sometimes occur.
  33. self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
  34. # Since the metadatachanged is defined in container stack, we can't use it here as a notifier for pyqt
  35. # properties. So we need to tie them together like this.
  36. self.metaDataChanged.connect(self.configuredConnectionTypesChanged)
  37. extrudersChanged = pyqtSignal()
  38. configuredConnectionTypesChanged = pyqtSignal()
  39. ## Get the list of extruders of this stack.
  40. #
  41. # \return The extruders registered with this stack.
  42. @pyqtProperty("QVariantMap", notify = extrudersChanged)
  43. def extruders(self) -> Dict[str, "ExtruderStack"]:
  44. return self._extruders
  45. @pyqtProperty("QVariantList", notify = extrudersChanged)
  46. def extruderList(self) -> List["ExtruderStack"]:
  47. result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0]))
  48. result_list = [item[1] for item in result_tuple_list]
  49. machine_extruder_count = self.getProperty("machine_extruder_count", "value")
  50. return result_list[:machine_extruder_count]
  51. @pyqtProperty(int, constant = True)
  52. def maxExtruderCount(self):
  53. return len(self.getMetaDataEntry("machine_extruder_trains"))
  54. @classmethod
  55. def getLoadingPriority(cls) -> int:
  56. return 2
  57. ## The configured connection types can be used to find out if the global
  58. # stack is configured to be connected with a printer, without having to
  59. # know all the details as to how this is exactly done (and without
  60. # actually setting the stack to be active).
  61. #
  62. # This data can then in turn also be used when the global stack is active;
  63. # If we can't get a network connection, but it is configured to have one,
  64. # we can display a different icon to indicate the difference.
  65. @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
  66. def configuredConnectionTypes(self) -> List[int]:
  67. # Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
  68. # But we do want them returned as a list of ints (so the rest of the code can directly compare)
  69. connection_types = self.getMetaDataEntry("connection_type", "").split(",")
  70. return [int(connection_type) for connection_type in connection_types if connection_type != ""]
  71. ## \sa configuredConnectionTypes
  72. def addConfiguredConnectionType(self, connection_type: int) -> None:
  73. configured_connection_types = self.configuredConnectionTypes
  74. if connection_type not in configured_connection_types:
  75. # Store the values as a string.
  76. configured_connection_types.append(connection_type)
  77. self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
  78. ## \sa configuredConnectionTypes
  79. def removeConfiguredConnectionType(self, connection_type: int) -> None:
  80. configured_connection_types = self.configuredConnectionTypes
  81. if connection_type in self.configured_connection_types:
  82. # Store the values as a string.
  83. configured_connection_types.remove(connection_type)
  84. self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
  85. @classmethod
  86. def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
  87. configuration_type = super().getConfigurationTypeFromSerialized(serialized)
  88. if configuration_type == "machine":
  89. return "machine_stack"
  90. return configuration_type
  91. def getBuildplateName(self) -> Optional[str]:
  92. name = None
  93. if self.variant.getId() != "empty_variant":
  94. name = self.variant.getName()
  95. return name
  96. @pyqtProperty(str, constant = True)
  97. def preferred_output_file_formats(self) -> str:
  98. return self.getMetaDataEntry("file_formats")
  99. ## Add an extruder to the list of extruders of this stack.
  100. #
  101. # \param extruder The extruder to add.
  102. #
  103. # \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we
  104. # already have the maximum number of extruders.
  105. def addExtruder(self, extruder: ContainerStack) -> None:
  106. position = extruder.getMetaDataEntry("position")
  107. if position is None:
  108. Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id)
  109. return
  110. if any(item.getId() == extruder.id for item in self._extruders.values()):
  111. Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self.getId())
  112. return
  113. self._extruders[position] = extruder
  114. self.extrudersChanged.emit()
  115. Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
  116. ## Overridden from ContainerStack
  117. #
  118. # This will return the value of the specified property for the specified setting,
  119. # unless the property is "value" and that setting has a "resolve" function set.
  120. # When a resolve is set, it will instead try and execute the resolve first and
  121. # then fall back to the normal "value" property.
  122. #
  123. # \param key The setting key to get the property of.
  124. # \param property_name The property to get the value of.
  125. #
  126. # \return The value of the property for the specified setting, or None if not found.
  127. @override(ContainerStack)
  128. def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
  129. if not self.definition.findDefinitions(key = key):
  130. return None
  131. if context is None:
  132. context = PropertyEvaluationContext()
  133. context.pushContainer(self)
  134. # Handle the "resolve" property.
  135. #TODO: Why the hell does this involve threading?
  136. # Answer: Because if multiple threads start resolving properties that have the same underlying properties that's
  137. # related, without taking a note of which thread a resolve paths belongs to, they can bump into each other and
  138. # generate unexpected behaviours.
  139. if self._shouldResolve(key, property_name, context):
  140. current_thread = threading.current_thread()
  141. self._resolving_settings[current_thread.name].add(key)
  142. resolve = super().getProperty(key, "resolve", context)
  143. self._resolving_settings[current_thread.name].remove(key)
  144. if resolve is not None:
  145. return resolve
  146. # Handle the "limit_to_extruder" property.
  147. limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
  148. if limit_to_extruder is not None:
  149. if limit_to_extruder == -1:
  150. limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
  151. limit_to_extruder = str(limit_to_extruder)
  152. if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
  153. if super().getProperty(key, "settable_per_extruder", context):
  154. result = self._extruders[str(limit_to_extruder)].getProperty(key, property_name, context)
  155. if result is not None:
  156. context.popContainer()
  157. return result
  158. else:
  159. Logger.log("e", "Setting {setting} has limit_to_extruder but is not settable per extruder!", setting = key)
  160. result = super().getProperty(key, property_name, context)
  161. context.popContainer()
  162. return result
  163. ## Overridden from ContainerStack
  164. #
  165. # This will simply raise an exception since the Global stack cannot have a next stack.
  166. @override(ContainerStack)
  167. def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
  168. raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
  169. # protected:
  170. # Determine whether or not we should try to get the "resolve" property instead of the
  171. # requested property.
  172. def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
  173. if property_name is not "value":
  174. # Do not try to resolve anything but the "value" property
  175. return False
  176. current_thread = threading.current_thread()
  177. if key in self._resolving_settings[current_thread.name]:
  178. # To prevent infinite recursion, if getProperty is called with the same key as
  179. # we are already trying to resolve, we should not try to resolve again. Since
  180. # this can happen multiple times when trying to resolve a value, we need to
  181. # track all settings that are being resolved.
  182. return False
  183. setting_state = super().getProperty(key, "state", context = context)
  184. if setting_state is not None and setting_state != InstanceState.Default:
  185. # When the user has explicitly set a value, we should ignore any resolve and
  186. # just return that value.
  187. return False
  188. return True
  189. ## Perform some sanity checks on the global stack
  190. # Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
  191. def isValid(self) -> bool:
  192. container_registry = ContainerRegistry.getInstance()
  193. extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId())
  194. machine_extruder_count = self.getProperty("machine_extruder_count", "value")
  195. extruder_check_position = set()
  196. for extruder_train in extruder_trains:
  197. extruder_position = extruder_train.getMetaDataEntry("position")
  198. extruder_check_position.add(extruder_position)
  199. for check_position in range(machine_extruder_count):
  200. if str(check_position) not in extruder_check_position:
  201. return False
  202. return True
  203. def getHeadAndFansCoordinates(self):
  204. return self.getProperty("machine_head_with_fans_polygon", "value")
  205. def getHasMaterials(self) -> bool:
  206. return parseBool(self.getMetaDataEntry("has_materials", False))
  207. def getHasVariants(self) -> bool:
  208. return parseBool(self.getMetaDataEntry("has_variants", False))
  209. def getHasMachineQuality(self) -> bool:
  210. return parseBool(self.getMetaDataEntry("has_machine_quality", False))
  211. ## Get default firmware file name if one is specified in the firmware
  212. @pyqtSlot(result = str)
  213. def getDefaultFirmwareName(self) -> str:
  214. machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")
  215. baudrate = 250000
  216. if Platform.isLinux():
  217. # Linux prefers a baudrate of 115200 here because older versions of
  218. # pySerial did not support a baudrate of 250000
  219. baudrate = 115200
  220. # If a firmware file is available, it should be specified in the definition for the printer
  221. hex_file = self.getMetaDataEntry("firmware_file", None)
  222. if machine_has_heated_bed:
  223. hex_file = self.getMetaDataEntry("firmware_hbk_file", hex_file)
  224. if not hex_file:
  225. Logger.log("w", "There is no firmware for machine %s.", self.getBottom().id)
  226. return ""
  227. try:
  228. return Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
  229. except FileNotFoundError:
  230. Logger.log("w", "Firmware file %s not found.", hex_file)
  231. return ""
  232. ## private:
  233. global_stack_mime = MimeType(
  234. name = "application/x-cura-globalstack",
  235. comment = "Cura Global Stack",
  236. suffixes = ["global.cfg"]
  237. )
  238. MimeTypeDatabase.addMimeType(global_stack_mime)
  239. ContainerRegistry.addContainerTypeByName(GlobalStack, "global_stack", global_stack_mime.name)