PrinterOutputDevice.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from enum import IntEnum
  4. from typing import Callable, List, Optional, Union
  5. from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
  6. from PyQt6.QtWidgets import QMessageBox
  7. import cura.CuraApplication # Imported like this to prevent circular imports.
  8. from UM.Logger import Logger
  9. from UM.Signal import signalemitter
  10. from UM.Qt.QtApplication import QtApplication
  11. from UM.FlameProfiler import pyqtSlot
  12. from UM.i18n import i18nCatalog
  13. from UM.OutputDevice.OutputDevice import OutputDevice
  14. MYPY = False
  15. if MYPY:
  16. from UM.FileHandler.FileHandler import FileHandler
  17. from UM.Scene.SceneNode import SceneNode
  18. from .Models.PrinterOutputModel import PrinterOutputModel
  19. from .Models.PrinterConfigurationModel import PrinterConfigurationModel
  20. from .FirmwareUpdater import FirmwareUpdater
  21. i18n_catalog = i18nCatalog("cura")
  22. class ConnectionState(IntEnum):
  23. """The current processing state of the backend."""
  24. Closed = 0
  25. Connecting = 1
  26. Connected = 2
  27. Busy = 3
  28. Error = 4
  29. class ConnectionType(IntEnum):
  30. NotConnected = 0
  31. UsbConnection = 1
  32. NetworkConnection = 2
  33. CloudConnection = 3
  34. @signalemitter
  35. class PrinterOutputDevice(QObject, OutputDevice):
  36. """Printer output device adds extra interface options on top of output device.
  37. The assumption is made the printer is a FDM printer.
  38. Note that a number of settings are marked as "final". This is because decorators
  39. are not inherited by children. To fix this we use the private counterpart of those
  40. functions to actually have the implementation.
  41. For all other uses it should be used in the same way as a "regular" OutputDevice.
  42. """
  43. printersChanged = pyqtSignal()
  44. connectionStateChanged = pyqtSignal(str)
  45. acceptsCommandsChanged = pyqtSignal()
  46. # Signal to indicate that the material of the active printer on the remote changed.
  47. materialIdChanged = pyqtSignal()
  48. # # Signal to indicate that the hotend of the active printer on the remote changed.
  49. hotendIdChanged = pyqtSignal()
  50. # Signal to indicate that the info text about the connection has changed.
  51. connectionTextChanged = pyqtSignal()
  52. # Signal to indicate that the configuration of one of the printers has changed.
  53. uniqueConfigurationsChanged = pyqtSignal()
  54. def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None:
  55. super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
  56. self._printers = [] # type: List[PrinterOutputModel]
  57. self._unique_configurations = [] # type: List[PrinterConfigurationModel]
  58. self._monitor_view_qml_path = "" # type: str
  59. self._monitor_component = None # type: Optional[QObject]
  60. self._monitor_item = None # type: Optional[QObject]
  61. self._control_view_qml_path = "" # type: str
  62. self._control_component = None # type: Optional[QObject]
  63. self._control_item = None # type: Optional[QObject]
  64. self._accepts_commands = False # type: bool
  65. self._update_timer = QTimer() # type: QTimer
  66. self._update_timer.setInterval(2000) # TODO; Add preference for update interval
  67. self._update_timer.setSingleShot(False)
  68. self._update_timer.timeout.connect(self._update)
  69. self._connection_state = ConnectionState.Closed # type: ConnectionState
  70. self._connection_type = connection_type # type: ConnectionType
  71. self._firmware_updater = None # type: Optional[FirmwareUpdater]
  72. self._firmware_name = None # type: Optional[str]
  73. self._address = "" # type: str
  74. self._connection_text = "" # type: str
  75. self.printersChanged.connect(self._onPrintersChanged)
  76. QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
  77. @pyqtProperty(str, notify = connectionTextChanged)
  78. def address(self) -> str:
  79. return self._address
  80. def setConnectionText(self, connection_text):
  81. if self._connection_text != connection_text:
  82. self._connection_text = connection_text
  83. self.connectionTextChanged.emit()
  84. @pyqtProperty(str, constant=True)
  85. def connectionText(self) -> str:
  86. return self._connection_text
  87. def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
  88. Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
  89. callback(QMessageBox.Yes)
  90. def isConnected(self) -> bool:
  91. """
  92. Returns whether we could theoretically send commands to this printer.
  93. :return: `True` if we are connected, or `False` if not.
  94. """
  95. return self.connectionState != ConnectionState.Closed and self.connectionState != ConnectionState.Error
  96. def setConnectionState(self, connection_state: "ConnectionState") -> None:
  97. """
  98. Store the connection state of the printer.
  99. Causes everything that displays the connection state to update its QML models.
  100. :param connection_state: The new connection state to store.
  101. """
  102. if self.connectionState != connection_state:
  103. self._connection_state = connection_state
  104. application = cura.CuraApplication.CuraApplication.getInstance()
  105. if application is not None: # Might happen during the closing of Cura or in a test.
  106. global_stack = application.getGlobalContainerStack()
  107. if global_stack is not None:
  108. global_stack.setMetaDataEntry("is_online", self.isConnected())
  109. self.connectionStateChanged.emit(self._id)
  110. @pyqtProperty(int, constant = True)
  111. def connectionType(self) -> "ConnectionType":
  112. return self._connection_type
  113. @pyqtProperty(int, notify = connectionStateChanged)
  114. def connectionState(self) -> "ConnectionState":
  115. """
  116. Get the connection state of the printer, e.g. whether it is connected, still connecting, error state, etc.
  117. :return: The current connection state of this output device.
  118. """
  119. return self._connection_state
  120. def _update(self) -> None:
  121. pass
  122. def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
  123. for printer in self._printers:
  124. if printer.key == key:
  125. return printer
  126. return None
  127. def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
  128. file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
  129. raise NotImplementedError("requestWrite needs to be implemented")
  130. @pyqtProperty(QObject, notify = printersChanged)
  131. def activePrinter(self) -> Optional["PrinterOutputModel"]:
  132. if self._printers:
  133. return self._printers[0]
  134. return None
  135. @pyqtProperty("QVariantList", notify = printersChanged)
  136. def printers(self) -> List["PrinterOutputModel"]:
  137. return self._printers
  138. @pyqtProperty(QObject, constant = True)
  139. def monitorItem(self) -> QObject:
  140. # Note that we specifically only check if the monitor component is created.
  141. # It could be that it failed to actually create the qml item! If we check if the item was created, it will try
  142. # to create the item (and fail) every time.
  143. if not self._monitor_component:
  144. self._createMonitorViewFromQML()
  145. return self._monitor_item
  146. @pyqtProperty(QObject, constant = True)
  147. def controlItem(self) -> QObject:
  148. if not self._control_component:
  149. self._createControlViewFromQML()
  150. return self._control_item
  151. def _createControlViewFromQML(self) -> None:
  152. if not self._control_view_qml_path:
  153. return
  154. if self._control_item is None:
  155. self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
  156. def _createMonitorViewFromQML(self) -> None:
  157. if not self._monitor_view_qml_path:
  158. return
  159. if self._monitor_item is None:
  160. self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
  161. def connect(self) -> None:
  162. """Attempt to establish connection"""
  163. self.setConnectionState(ConnectionState.Connecting)
  164. self._update_timer.start()
  165. def close(self) -> None:
  166. """Attempt to close the connection"""
  167. self._update_timer.stop()
  168. self.setConnectionState(ConnectionState.Closed)
  169. def __del__(self) -> None:
  170. """Ensure that close gets called when object is destroyed"""
  171. self.close()
  172. @pyqtProperty(bool, notify = acceptsCommandsChanged)
  173. def acceptsCommands(self) -> bool:
  174. return self._accepts_commands
  175. def _setAcceptsCommands(self, accepts_commands: bool) -> None:
  176. """Set a flag to signal the UI that the printer is not (yet) ready to receive commands"""
  177. if self._accepts_commands != accepts_commands:
  178. self._accepts_commands = accepts_commands
  179. self.acceptsCommandsChanged.emit()
  180. @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
  181. def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
  182. """ Returns the unique configurations of the printers within this output device """
  183. return self._unique_configurations
  184. def _updateUniqueConfigurations(self) -> None:
  185. all_configurations = set()
  186. for printer in self._printers:
  187. if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
  188. all_configurations.add(printer.printerConfiguration)
  189. all_configurations.update(printer.availableConfigurations)
  190. if None in all_configurations:
  191. # Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration.
  192. # List could end up empty!
  193. Logger.log("e", "Found a broken configuration in the synced list!")
  194. all_configurations.remove(None)
  195. new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "", reverse = True)
  196. if new_configurations != self._unique_configurations:
  197. self._unique_configurations = new_configurations
  198. self.uniqueConfigurationsChanged.emit()
  199. @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
  200. def uniquePrinterTypes(self) -> List[str]:
  201. """ Returns the unique configurations of the printers within this output device """
  202. return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
  203. def _onPrintersChanged(self) -> None:
  204. for printer in self._printers:
  205. printer.configurationChanged.connect(self._updateUniqueConfigurations)
  206. printer.availableConfigurationsChanged.connect(self._updateUniqueConfigurations)
  207. # At this point there may be non-updated configurations
  208. self._updateUniqueConfigurations()
  209. def _setFirmwareName(self, name: str) -> None:
  210. """Set the device firmware name
  211. :param name: The name of the firmware.
  212. """
  213. self._firmware_name = name
  214. def getFirmwareName(self) -> Optional[str]:
  215. """Get the name of device firmware
  216. This name can be used to define device type
  217. """
  218. return self._firmware_name
  219. def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
  220. return self._firmware_updater
  221. @pyqtSlot(str)
  222. def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
  223. if not self._firmware_updater:
  224. return
  225. self._firmware_updater.updateFirmware(firmware_file)