GlobalStack.py 16 KB

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