PrinterOutputDevice.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.Decorators import deprecated
  4. from UM.i18n import i18nCatalog
  5. from UM.OutputDevice.OutputDevice import OutputDevice
  6. from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
  7. from PyQt5.QtWidgets import QMessageBox
  8. from UM.Logger import Logger
  9. from UM.FileHandler.FileHandler import FileHandler #For typing.
  10. from UM.Scene.SceneNode import SceneNode #For typing.
  11. from UM.Signal import signalemitter
  12. from UM.Qt.QtApplication import QtApplication
  13. from enum import IntEnum # For the connection state tracking.
  14. from typing import Callable, List, Optional
  15. MYPY = False
  16. if MYPY:
  17. from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
  18. from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
  19. from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
  20. i18n_catalog = i18nCatalog("cura")
  21. ## The current processing state of the backend.
  22. class ConnectionState(IntEnum):
  23. closed = 0
  24. connecting = 1
  25. connected = 2
  26. busy = 3
  27. error = 4
  28. ## Printer output device adds extra interface options on top of output device.
  29. #
  30. # The assumption is made the printer is a FDM printer.
  31. #
  32. # Note that a number of settings are marked as "final". This is because decorators
  33. # are not inherited by children. To fix this we use the private counter part of those
  34. # functions to actually have the implementation.
  35. #
  36. # For all other uses it should be used in the same way as a "regular" OutputDevice.
  37. @signalemitter
  38. class PrinterOutputDevice(QObject, OutputDevice):
  39. printersChanged = pyqtSignal()
  40. connectionStateChanged = pyqtSignal(str)
  41. acceptsCommandsChanged = pyqtSignal()
  42. # Signal to indicate that the material of the active printer on the remote changed.
  43. materialIdChanged = pyqtSignal()
  44. # # Signal to indicate that the hotend of the active printer on the remote changed.
  45. hotendIdChanged = pyqtSignal()
  46. # Signal to indicate that the info text about the connection has changed.
  47. connectionTextChanged = pyqtSignal()
  48. # Signal to indicate that the configuration of one of the printers has changed.
  49. uniqueConfigurationsChanged = pyqtSignal()
  50. def __init__(self, device_id: str, parent: QObject = None) -> None:
  51. super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
  52. self._printers = [] # type: List[PrinterOutputModel]
  53. self._unique_configurations = [] # type: List[ConfigurationModel]
  54. self._monitor_view_qml_path = "" #type: str
  55. self._monitor_component = None #type: Optional[QObject]
  56. self._monitor_item = None #type: Optional[QObject]
  57. self._control_view_qml_path = "" #type: str
  58. self._control_component = None #type: Optional[QObject]
  59. self._control_item = None #type: Optional[QObject]
  60. self._accepts_commands = False #type: bool
  61. self._update_timer = QTimer() #type: QTimer
  62. self._update_timer.setInterval(2000) # TODO; Add preference for update interval
  63. self._update_timer.setSingleShot(False)
  64. self._update_timer.timeout.connect(self._update)
  65. self._connection_state = ConnectionState.closed #type: ConnectionState
  66. self._firmware_updater = None #type: Optional[FirmwareUpdater]
  67. self._firmware_name = None #type: Optional[str]
  68. self._address = "" #type: str
  69. self._connection_text = "" #type: str
  70. self.printersChanged.connect(self._onPrintersChanged)
  71. QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
  72. @pyqtProperty(str, notify = connectionTextChanged)
  73. def address(self) -> str:
  74. return self._address
  75. def setConnectionText(self, connection_text):
  76. if self._connection_text != connection_text:
  77. self._connection_text = connection_text
  78. self.connectionTextChanged.emit()
  79. @pyqtProperty(str, constant=True)
  80. def connectionText(self) -> str:
  81. return self._connection_text
  82. def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
  83. Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
  84. callback(QMessageBox.Yes)
  85. def isConnected(self) -> bool:
  86. return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
  87. def setConnectionState(self, connection_state: ConnectionState) -> None:
  88. if self._connection_state != connection_state:
  89. self._connection_state = connection_state
  90. self.connectionStateChanged.emit(self._id)
  91. @pyqtProperty(str, notify = connectionStateChanged)
  92. def connectionState(self) -> ConnectionState:
  93. return self._connection_state
  94. def _update(self) -> None:
  95. pass
  96. def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
  97. for printer in self._printers:
  98. if printer.key == key:
  99. return printer
  100. return None
  101. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
  102. raise NotImplementedError("requestWrite needs to be implemented")
  103. @pyqtProperty(QObject, notify = printersChanged)
  104. def activePrinter(self) -> Optional["PrinterOutputModel"]:
  105. if len(self._printers):
  106. return self._printers[0]
  107. return None
  108. @pyqtProperty("QVariantList", notify = printersChanged)
  109. def printers(self) -> List["PrinterOutputModel"]:
  110. return self._printers
  111. @pyqtProperty(QObject, constant = True)
  112. def monitorItem(self) -> QObject:
  113. # Note that we specifically only check if the monitor component is created.
  114. # It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
  115. # create the item (and fail) every time.
  116. if not self._monitor_component:
  117. self._createMonitorViewFromQML()
  118. return self._monitor_item
  119. @pyqtProperty(QObject, constant = True)
  120. def controlItem(self) -> QObject:
  121. if not self._control_component:
  122. self._createControlViewFromQML()
  123. return self._control_item
  124. def _createControlViewFromQML(self) -> None:
  125. if not self._control_view_qml_path:
  126. return
  127. if self._control_item is None:
  128. self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
  129. def _createMonitorViewFromQML(self) -> None:
  130. if not self._monitor_view_qml_path:
  131. return
  132. if self._monitor_item is None:
  133. self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
  134. ## Attempt to establish connection
  135. def connect(self) -> None:
  136. self.setConnectionState(ConnectionState.connecting)
  137. self._update_timer.start()
  138. ## Attempt to close the connection
  139. def close(self) -> None:
  140. self._update_timer.stop()
  141. self.setConnectionState(ConnectionState.closed)
  142. ## Ensure that close gets called when object is destroyed
  143. def __del__(self) -> None:
  144. self.close()
  145. @pyqtProperty(bool, notify = acceptsCommandsChanged)
  146. def acceptsCommands(self) -> bool:
  147. return self._accepts_commands
  148. @deprecated("Please use the protected function instead", "3.2")
  149. def setAcceptsCommands(self, accepts_commands: bool) -> None:
  150. self._setAcceptsCommands(accepts_commands)
  151. ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
  152. def _setAcceptsCommands(self, accepts_commands: bool) -> None:
  153. if self._accepts_commands != accepts_commands:
  154. self._accepts_commands = accepts_commands
  155. self.acceptsCommandsChanged.emit()
  156. # Returns the unique configurations of the printers within this output device
  157. @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
  158. def uniqueConfigurations(self) -> List["ConfigurationModel"]:
  159. return self._unique_configurations
  160. def _updateUniqueConfigurations(self) -> None:
  161. self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None]))
  162. self._unique_configurations.sort(key = lambda k: k.printerType)
  163. self.uniqueConfigurationsChanged.emit()
  164. def _onPrintersChanged(self) -> None:
  165. for printer in self._printers:
  166. printer.configurationChanged.connect(self._updateUniqueConfigurations)
  167. # At this point there may be non-updated configurations
  168. self._updateUniqueConfigurations()
  169. ## Set the device firmware name
  170. #
  171. # \param name The name of the firmware.
  172. def _setFirmwareName(self, name: str) -> None:
  173. self._firmware_name = name
  174. ## Get the name of device firmware
  175. #
  176. # This name can be used to define device type
  177. def getFirmwareName(self) -> Optional[str]:
  178. return self._firmware_name
  179. def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
  180. return self._firmware_updater