1# Copyright (c) 2019 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3from itertools import product
4from typing import List, Union, Dict, Optional, Any
5
6from PyQt5.QtCore import QUrl
7
8from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
9from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
10from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
11
12from .ClusterBuildPlate import ClusterBuildPlate
13from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration
14from .ClusterPrinterMaterialStation import ClusterPrinterMaterialStation
15from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot
16from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMaterial
17from ..BaseModel import BaseModel
18
19
20class ClusterPrinterStatus(BaseModel):
21    """Class representing a cluster printer"""
22
23
24    def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str,
25                 status: str, unique_name: str, uuid: str,
26                 configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]],
27                 reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None,
28                 firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None,
29                 build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None,
30                 material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None:
31        """Creates a new cluster printer status
32
33        :param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled.
34        :param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster.
35        :param friendly_name: Human readable name of the printer. Can be used for identification purposes.
36        :param ip_address: The IP address of the printer in the local network.
37        :param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'.
38        :param status: The status of the printer.
39        :param unique_name: The unique name of the printer in the network.
40        :param uuid: The unique ID of the printer, also known as GUID.
41        :param configuration: The active print core configurations of this printer.
42        :param reserved_by: A printer can be claimed by a specific print job.
43        :param maintenance_required: Indicates if maintenance is necessary.
44        :param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date",
45          "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible".
46        :param latest_available_firmware: The version of the latest firmware that is available.
47        :param build_plate: The build plate that is on the printer.
48        :param material_station: The material station that is on the printer.
49        """
50
51        self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration)
52        self.enabled = enabled
53        self.firmware_version = firmware_version
54        self.friendly_name = friendly_name
55        self.ip_address = ip_address
56        self.machine_variant = machine_variant
57        self.status = status
58        self.unique_name = unique_name
59        self.uuid = uuid
60        self.reserved_by = reserved_by
61        self.maintenance_required = maintenance_required
62        self.firmware_update_status = firmware_update_status
63        self.latest_available_firmware = latest_available_firmware
64        self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None
65        self.material_station = self.parseModel(ClusterPrinterMaterialStation,
66                                                material_station) if material_station else None
67        super().__init__(**kwargs)
68
69    def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel:
70        """Creates a new output model.
71
72        :param controller: - The controller of the model.
73        """
74        model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version)
75        self.updateOutputModel(model)
76        return model
77
78    def updateOutputModel(self, model: PrinterOutputModel) -> None:
79        """Updates the given output model.
80
81        :param model: - The output model to update.
82        """
83
84        model.updateKey(self.uuid)
85        model.updateName(self.friendly_name)
86        model.updateUniqueName(self.unique_name)
87        model.updateType(self.machine_variant)
88        model.updateState(self.status if self.enabled else "disabled")
89        model.updateBuildplate(self.build_plate.type if self.build_plate else "glass")
90        model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address)))
91
92        if not model.printerConfiguration:
93            # Prevent accessing printer configuration when not available.
94            # This sometimes happens when a printer was just added to a group and Cura is connected to that group.
95            return
96
97        # Set the possible configurations based on whether a Material Station is present or not.
98        if self.material_station and self.material_station.material_slots:
99            self._updateAvailableConfigurations(model)
100        if self.configuration:
101            self._updateActiveConfiguration(model)
102
103    def _updateActiveConfiguration(self, model: PrinterOutputModel) -> None:
104        configurations = zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations)
105        for configuration, extruder_output, extruder_config in configurations:
106            configuration.updateOutputModel(extruder_output)
107            configuration.updateConfigurationModel(extruder_config)
108
109    def _updateAvailableConfigurations(self, model: PrinterOutputModel) -> None:
110        available_configurations = [self._createAvailableConfigurationFromPrinterConfiguration(
111            left_slot = left_slot,
112            right_slot = right_slot,
113            printer_configuration = model.printerConfiguration
114        ) for left_slot, right_slot in product(self._getSlotsForExtruder(0), self._getSlotsForExtruder(1))]
115        model.setAvailableConfigurations(available_configurations)
116
117    def _getSlotsForExtruder(self, extruder_index: int) -> List[ClusterPrinterMaterialStationSlot]:
118        """Create a list of Material Station slots for the given extruder index.
119
120        Returns a list with a single empty material slot if none are found to ensure we don't miss configurations.
121        """
122
123        if not self.material_station:  # typing guard
124            return []
125        slots = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration(
126            slot = slot,
127            extruder_index = extruder_index
128        )]
129        return slots or [self._createEmptyMaterialSlot(extruder_index)]
130
131    @staticmethod
132    def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool:
133        """Check if a configuration is supported in order to make it selectable by the user.
134
135        We filter out any slot that is not supported by the extruder index, print core type or if the material is empty.
136        """
137
138        return slot.extruder_index == extruder_index and slot.compatible and not slot.material_empty
139
140    @staticmethod
141    def _createEmptyMaterialSlot(extruder_index: int) -> ClusterPrinterMaterialStationSlot:
142        """Create an empty material slot with a fake empty material."""
143
144        empty_material = ClusterPrinterConfigurationMaterial(guid = "", material = "empty", brand = "", color = "")
145        return ClusterPrinterMaterialStationSlot(slot_index = 0, extruder_index = extruder_index,
146                                                 compatible = True, material_remaining = 0, material = empty_material)
147
148    @staticmethod
149    def _createAvailableConfigurationFromPrinterConfiguration(left_slot: ClusterPrinterMaterialStationSlot,
150                                                              right_slot: ClusterPrinterMaterialStationSlot,
151                                                              printer_configuration: PrinterConfigurationModel
152                                                              ) -> PrinterConfigurationModel:
153        available_configuration = PrinterConfigurationModel()
154        available_configuration.setExtruderConfigurations([left_slot.createConfigurationModel(),
155                                                           right_slot.createConfigurationModel()])
156        available_configuration.setPrinterType(printer_configuration.printerType)
157        available_configuration.setBuildplateConfiguration(printer_configuration.buildplateConfiguration)
158        return available_configuration
159