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