DiscoveredPrintersModel.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Callable, Dict, List, Optional, TYPE_CHECKING
  4. from PyQt6.QtCore import pyqtSlot, pyqtProperty, pyqtSignal, QObject, QTimer
  5. from UM.i18n import i18nCatalog
  6. from UM.Logger import Logger
  7. from UM.Util import parseBool
  8. from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
  9. if TYPE_CHECKING:
  10. from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
  11. from cura.CuraApplication import CuraApplication
  12. from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
  13. catalog = i18nCatalog("cura")
  14. class DiscoveredPrinter(QObject):
  15. def __init__(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None], machine_type: str,
  16. device: "NetworkedPrinterOutputDevice", parent: Optional["QObject"] = None) -> None:
  17. super().__init__(parent)
  18. self._ip_address = ip_address
  19. self._key = key
  20. self._name = name
  21. self.create_callback = create_callback
  22. self._machine_type = machine_type
  23. self._device = device
  24. nameChanged = pyqtSignal()
  25. def getKey(self) -> str:
  26. return self._key
  27. @pyqtProperty(str, notify = nameChanged)
  28. def name(self) -> str:
  29. return self._name
  30. def setName(self, name: str) -> None:
  31. if self._name != name:
  32. self._name = name
  33. self.nameChanged.emit()
  34. @pyqtProperty(str, constant = True)
  35. def address(self) -> str:
  36. return self._ip_address
  37. machineTypeChanged = pyqtSignal()
  38. @pyqtProperty(str, notify = machineTypeChanged)
  39. def machineType(self) -> str:
  40. return self._machine_type
  41. def setMachineType(self, machine_type: str) -> None:
  42. if self._machine_type != machine_type:
  43. self._machine_type = machine_type
  44. self.machineTypeChanged.emit()
  45. # Checks if the given machine type name in the available machine list.
  46. # The machine type is a code name such as "ultimaker_3", while the machine type name is the human-readable name of
  47. # the machine type, which is "Ultimaker 3" for "ultimaker_3".
  48. def _hasHumanReadableMachineTypeName(self, machine_type_name: str) -> bool:
  49. from cura.CuraApplication import CuraApplication
  50. results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(name = machine_type_name)
  51. return len(results) > 0
  52. # Human readable machine type string
  53. @pyqtProperty(str, notify = machineTypeChanged)
  54. def readableMachineType(self) -> str:
  55. # In NetworkOutputDevice, when it updates a printer information, it updates the machine type using the field
  56. # "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
  57. # like "Ultimaker 3". The code below handles this case.
  58. if self._hasHumanReadableMachineTypeName(self._machine_type):
  59. readable_type = self._machine_type
  60. else:
  61. readable_type = self._getMachineTypeNameFromId(self._machine_type)
  62. if not readable_type:
  63. readable_type = catalog.i18nc("@label", "Unknown")
  64. return readable_type
  65. @pyqtProperty(bool, notify = machineTypeChanged)
  66. def isUnknownMachineType(self) -> bool:
  67. if self._hasHumanReadableMachineTypeName(self._machine_type):
  68. readable_type = self._machine_type
  69. else:
  70. readable_type = self._getMachineTypeNameFromId(self._machine_type)
  71. return not readable_type
  72. def _getMachineTypeNameFromId(self, machine_type_id: str) -> str:
  73. machine_type_name = ""
  74. from cura.CuraApplication import CuraApplication
  75. results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(id = machine_type_id)
  76. if results:
  77. machine_type_name = results[0]["name"]
  78. return machine_type_name
  79. @pyqtProperty(QObject, constant = True)
  80. def device(self) -> "NetworkedPrinterOutputDevice":
  81. return self._device
  82. @pyqtProperty(bool, constant = True)
  83. def isHostOfGroup(self) -> bool:
  84. return getattr(self._device, "clusterSize", 1) > 0
  85. @pyqtProperty(str, constant = True)
  86. def sectionName(self) -> str:
  87. if self.isUnknownMachineType or not self.isHostOfGroup:
  88. return catalog.i18nc("@label", "The printer(s) below cannot be connected because they are part of a group")
  89. else:
  90. return catalog.i18nc("@label", "Available networked printers")
  91. class DiscoveredPrintersModel(QObject):
  92. """Discovered printers are all the printers that were found on the network, which provide a more convenient way to
  93. add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then add
  94. that printer to Cura as the active one).
  95. """
  96. def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
  97. super().__init__(parent)
  98. self._application = application
  99. self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
  100. self._plugin_for_manual_device = None # type: Optional[OutputDevicePlugin]
  101. self._network_plugin_queue = [] # type: List[OutputDevicePlugin]
  102. self._manual_device_address = ""
  103. self._manual_device_request_timeout_in_seconds = 5 # timeout for adding a manual device in seconds
  104. self._manual_device_request_timer = QTimer()
  105. self._manual_device_request_timer.setInterval(self._manual_device_request_timeout_in_seconds * 1000)
  106. self._manual_device_request_timer.setSingleShot(True)
  107. self._manual_device_request_timer.timeout.connect(self._onManualRequestTimeout)
  108. discoveredPrintersChanged = pyqtSignal()
  109. @pyqtSlot(str)
  110. def checkManualDevice(self, address: str) -> None:
  111. if self.hasManualDeviceRequestInProgress:
  112. Logger.log("i", "A manual device request for address [%s] is still in progress, do nothing",
  113. self._manual_device_address)
  114. return
  115. priority_order = [
  116. ManualDeviceAdditionAttempt.PRIORITY,
  117. ManualDeviceAdditionAttempt.POSSIBLE,
  118. ] # type: List[ManualDeviceAdditionAttempt]
  119. all_plugins_dict = self._application.getOutputDeviceManager().getAllOutputDevicePlugins()
  120. self._network_plugin_queue = [item for item in filter(
  121. lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order,
  122. all_plugins_dict.values())]
  123. if not self._network_plugin_queue:
  124. Logger.log("d", "Could not find a plugin to accept adding %s manually via address.", address)
  125. return
  126. self._attemptToAddManualDevice(address)
  127. def _attemptToAddManualDevice(self, address: str) -> None:
  128. if self._network_plugin_queue:
  129. self._plugin_for_manual_device = self._network_plugin_queue.pop()
  130. Logger.log("d", "Network plugin %s: attempting to add manual device with address %s.",
  131. self._plugin_for_manual_device.getId(), address)
  132. self._plugin_for_manual_device.addManualDevice(address, callback=self._onManualDeviceRequestFinished)
  133. self._manual_device_address = address
  134. self._manual_device_request_timer.start()
  135. self.hasManualDeviceRequestInProgressChanged.emit()
  136. @pyqtSlot()
  137. def cancelCurrentManualDeviceRequest(self) -> None:
  138. self._manual_device_request_timer.stop()
  139. if self._manual_device_address:
  140. if self._plugin_for_manual_device is not None:
  141. self._plugin_for_manual_device.removeManualDevice(self._manual_device_address, address = self._manual_device_address)
  142. self._manual_device_address = ""
  143. self._plugin_for_manual_device = None
  144. self.hasManualDeviceRequestInProgressChanged.emit()
  145. self.manualDeviceRequestFinished.emit(False)
  146. def _onManualRequestTimeout(self) -> None:
  147. address = self._manual_device_address
  148. Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", address)
  149. self.cancelCurrentManualDeviceRequest()
  150. if self._network_plugin_queue:
  151. self._attemptToAddManualDevice(address)
  152. hasManualDeviceRequestInProgressChanged = pyqtSignal()
  153. @pyqtProperty(bool, notify = hasManualDeviceRequestInProgressChanged)
  154. def hasManualDeviceRequestInProgress(self) -> bool:
  155. return self._manual_device_address != ""
  156. manualDeviceRequestFinished = pyqtSignal(bool, arguments = ["success"])
  157. def _onManualDeviceRequestFinished(self, success: bool, address: str) -> None:
  158. self._manual_device_request_timer.stop()
  159. if address == self._manual_device_address:
  160. self._manual_device_address = ""
  161. self.hasManualDeviceRequestInProgressChanged.emit()
  162. self.manualDeviceRequestFinished.emit(success)
  163. if not success and self._network_plugin_queue:
  164. self._attemptToAddManualDevice(address)
  165. @pyqtProperty("QVariantMap", notify = discoveredPrintersChanged)
  166. def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]:
  167. return self._discovered_printer_by_ip_dict
  168. @pyqtProperty("QVariantList", notify = discoveredPrintersChanged)
  169. def discoveredPrinters(self) -> List["DiscoveredPrinter"]:
  170. item_list = list(
  171. x for x in self._discovered_printer_by_ip_dict.values() if not parseBool(x.device.getProperty("temporary")))
  172. # Split the printers into 2 lists and sort them ascending based on names.
  173. available_list = []
  174. not_available_list = []
  175. for item in item_list:
  176. if item.isUnknownMachineType or getattr(item.device, "clusterSize", 1) < 1:
  177. not_available_list.append(item)
  178. else:
  179. available_list.append(item)
  180. available_list.sort(key = lambda x: x.device.name)
  181. not_available_list.sort(key = lambda x: x.device.name)
  182. return available_list + not_available_list
  183. def addDiscoveredPrinter(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None],
  184. machine_type: str, device: "NetworkedPrinterOutputDevice") -> None:
  185. if ip_address in self._discovered_printer_by_ip_dict:
  186. Logger.log("e", "Printer with ip [%s] has already been added", ip_address)
  187. return
  188. discovered_printer = DiscoveredPrinter(ip_address, key, name, create_callback, machine_type, device, parent = self)
  189. self._discovered_printer_by_ip_dict[ip_address] = discovered_printer
  190. self.discoveredPrintersChanged.emit()
  191. def updateDiscoveredPrinter(self, ip_address: str,
  192. name: Optional[str] = None,
  193. machine_type: Optional[str] = None) -> None:
  194. if ip_address not in self._discovered_printer_by_ip_dict:
  195. Logger.log("w", "Printer with ip [%s] is not known", ip_address)
  196. return
  197. item = self._discovered_printer_by_ip_dict[ip_address]
  198. if name is not None:
  199. item.setName(name)
  200. if machine_type is not None:
  201. item.setMachineType(machine_type)
  202. def removeDiscoveredPrinter(self, ip_address: str) -> None:
  203. if ip_address not in self._discovered_printer_by_ip_dict:
  204. Logger.log("w", "Key [%s] does not exist in the discovered printers list.", ip_address)
  205. return
  206. del self._discovered_printer_by_ip_dict[ip_address]
  207. self.discoveredPrintersChanged.emit()
  208. @pyqtSlot("QVariant")
  209. def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None:
  210. """A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
  211. This function invokes the given discovered printer's "create_callback" to do this
  212. :param discovered_printer:
  213. """
  214. discovered_printer.create_callback(discovered_printer.getKey())