DiscoveredPrintersModel.py 12 KB

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