PrinterOutputDevice.py 11 KB

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