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