From 3578afd4ac2b94beea703f1534c06f422cfa1417 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 22 Aug 2019 23:47:07 +0200 Subject: [PATCH 1/7] Add support for multiple available configurations via network and cloud --- plugins/ThingiBrowser | 1 + .../Http/ClusterPrintCoreConfiguration.py | 5 +- .../Http/ClusterPrinterMaterialStation.py | 23 ++++++ .../Http/ClusterPrinterMaterialStationSlot.py | 17 ++++ .../src/Models/Http/ClusterPrinterStatus.py | 77 ++++++++++++++++--- 5 files changed, 111 insertions(+), 12 deletions(-) create mode 120000 plugins/ThingiBrowser create mode 100644 plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py create mode 100644 plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py diff --git a/plugins/ThingiBrowser b/plugins/ThingiBrowser new file mode 120000 index 0000000000..8126b65fd5 --- /dev/null +++ b/plugins/ThingiBrowser @@ -0,0 +1 @@ +/Users/chris/Code/ChrisTerBeke/ThingiBrowser \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index 24c9a577f9..e11d2be2d2 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -9,7 +9,8 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate from ..BaseModel import BaseModel -## Class representing a cloud cluster printer configuration +## Class representing a cloud cluster printer configuration +# Also used for representing slots in a Material Station (as from Cura's perspective these are the same). class ClusterPrintCoreConfiguration(BaseModel): ## Creates a new cloud cluster printer configuration object @@ -18,7 +19,7 @@ class ClusterPrintCoreConfiguration(BaseModel): # \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. # \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'. def __init__(self, extruder_index: int, - material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial], + material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial] = None, print_core_id: Optional[str] = None, **kwargs) -> None: self.extruder_index = extruder_index self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py new file mode 100644 index 0000000000..295044b957 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py @@ -0,0 +1,23 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from typing import Union, Dict, Any, List + +from ..BaseModel import BaseModel +from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot + + +## Class representing the data of a Material Station in the cluster. +class ClusterPrinterMaterialStation(BaseModel): + + ## Creates a new Material Station status. + # \param status: The status of the material station. + # \param: supported: Whether the material station is supported on this machine or not. + # \param material_slots: The active slots configurations of this material station. + def __init__(self, status: str, supported: bool = False, + material_slots: Union[None, Dict[str, Any], ClusterPrinterMaterialStationSlot] = None, + **kwargs) -> None: + self.status = status + self.supported = supported + self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\ + if material_slots else [] # type: List[ClusterPrinterMaterialStationSlot] + super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py new file mode 100644 index 0000000000..2e6bb6e7a5 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py @@ -0,0 +1,17 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration + + +## Class representing the data of a single slot in the material station. +class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration): + + ## Create a new material station slot object. + # \param slot_index: The index of the slot in the material station (ranging 0 to 5). + # \param compatible: Whether the configuration is compatible with the print core. + # \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data). + def __init__(self, slot_index: int, compatible: bool, material_remaining: float, **kwargs): + self.slot_index = slot_index + self.compatible = compatible + self.material_remaining = material_remaining + super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 7ab2082451..6e971e2bd3 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -1,14 +1,18 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from itertools import product from typing import List, Union, Dict, Optional, Any from PyQt5.QtCore import QUrl +from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from .ClusterBuildPlate import ClusterBuildPlate from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration +from .ClusterPrinterMaterialStation import ClusterPrinterMaterialStation +from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot from ..BaseModel import BaseModel @@ -26,17 +30,19 @@ class ClusterPrinterStatus(BaseModel): # \param uuid: The unique ID of the printer, also known as GUID. # \param configuration: The active print core configurations of this printer. # \param reserved_by: A printer can be claimed by a specific print job. - # \param maintenance_required: Indicates if maintenance is necessary + # \param maintenance_required: Indicates if maintenance is necessary. # \param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date", - # "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible" - # \param latest_available_firmware: The version of the latest firmware that is available - # \param build_plate: The build plate that is on the printer + # "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible". + # \param latest_available_firmware: The version of the latest firmware that is available. + # \param build_plate: The build plate that is on the printer. + # \param material_station: The material station that is on the printer. def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, status: str, unique_name: str, uuid: str, configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None, firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, - build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, **kwargs) -> None: + build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, + material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.enabled = enabled @@ -52,6 +58,8 @@ class ClusterPrinterStatus(BaseModel): self.firmware_update_status = firmware_update_status self.latest_available_firmware = latest_available_firmware self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None + self.material_station = self.parseModel(ClusterPrinterMaterialStation, + material_station) if material_station else None super().__init__(**kwargs) ## Creates a new output model. @@ -71,8 +79,57 @@ class ClusterPrinterStatus(BaseModel): model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) - if model.printerConfiguration is not None: - for configuration, extruder_output, extruder_config in \ - zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations): - configuration.updateOutputModel(extruder_output) - configuration.updateConfigurationModel(extruder_config) + # Set the possible configurations based on whether a Material Station is present or not. + if self.material_station is not None: + self._updateAvailableConfigurations(model) + if self.configuration is not None: + self._updateActiveConfiguration(model) + + def _updateActiveConfiguration(self, model: PrinterOutputModel) -> None: + configurations = zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations) + for configuration, extruder_output, extruder_config in configurations: + configuration.updateOutputModel(extruder_output) + configuration.updateConfigurationModel(extruder_config) + + def _updateAvailableConfigurations(self, model: PrinterOutputModel) -> None: + + # Generate a list of configurations for the left extruder. + left_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( + slot = slot, + extruder_index = 0 + )] + + # Generate a list of configurations for the right extruder. + right_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( + slot = slot, + extruder_index = 1 + )] + + # Create a list of all available combinations between both print cores. + available_configurations = [self._createAvailableConfigurationFromPrinterConfiguration( + left_slot = left_slot, + right_slot = right_slot, + printer_configuration = model.printerConfiguration + ) for left_slot, right_slot in product(left_configurations, right_configurations)] + + # Let Cura know which available configurations there are. + model.setAvailableConfigurations(available_configurations) + + ## Check if a configuration is supported in order to make it selectable by the user. + # We filter out any slot that is not supported by the extruder index, print core type or if the material is empty. + @staticmethod + def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool: + return slot.extruder_index == extruder_index and slot.compatible and slot.material and \ + slot.material_remaining != 0 + + @staticmethod + def _createAvailableConfigurationFromPrinterConfiguration(left_slot: ClusterPrinterMaterialStationSlot, + right_slot: ClusterPrinterMaterialStationSlot, + printer_configuration: PrinterConfigurationModel + ) -> PrinterConfigurationModel: + available_configuration = PrinterConfigurationModel() + available_configuration.setExtruderConfigurations([left_slot.createConfigurationModel(), + right_slot.createConfigurationModel()]) + available_configuration.setPrinterType(printer_configuration.printerType) + available_configuration.setBuildplateConfiguration(printer_configuration.buildplateConfiguration) + return available_configuration From d674494cdba9332fce4068c51e07338936d34ab8 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 10:10:44 +0200 Subject: [PATCH 2/7] Don't look at available configurations when no slots are available --- plugins/ThingiBrowser | 1 - .../UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 120000 plugins/ThingiBrowser diff --git a/plugins/ThingiBrowser b/plugins/ThingiBrowser deleted file mode 120000 index 8126b65fd5..0000000000 --- a/plugins/ThingiBrowser +++ /dev/null @@ -1 +0,0 @@ -/Users/chris/Code/ChrisTerBeke/ThingiBrowser \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 6e971e2bd3..323ecf5c75 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -80,7 +80,7 @@ class ClusterPrinterStatus(BaseModel): model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) # Set the possible configurations based on whether a Material Station is present or not. - if self.material_station is not None: + if self.material_station is not None and len(self.material_station.material_slots): self._updateAvailableConfigurations(model) if self.configuration is not None: self._updateActiveConfiguration(model) From c491d7f3e2864a6b552082db5195cc462a8a4493 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 10:40:17 +0200 Subject: [PATCH 3/7] Some reformatting --- .../src/Models/Http/ClusterPrinterStatus.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 323ecf5c75..841cfd9fa1 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -92,26 +92,22 @@ class ClusterPrinterStatus(BaseModel): configuration.updateConfigurationModel(extruder_config) def _updateAvailableConfigurations(self, model: PrinterOutputModel) -> None: - # Generate a list of configurations for the left extruder. left_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( slot = slot, extruder_index = 0 )] - # Generate a list of configurations for the right extruder. right_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( slot = slot, extruder_index = 1 )] - # Create a list of all available combinations between both print cores. available_configurations = [self._createAvailableConfigurationFromPrinterConfiguration( left_slot = left_slot, right_slot = right_slot, printer_configuration = model.printerConfiguration ) for left_slot, right_slot in product(left_configurations, right_configurations)] - # Let Cura know which available configurations there are. model.setAvailableConfigurations(available_configurations) From 36f6dba2fcbb6f6c36304025233329a42258f5f4 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 16:23:51 +0200 Subject: [PATCH 4/7] Fix not displaying configuration with both extruders empty --- cura/PrinterOutput/Models/PrinterConfigurationModel.py | 8 ++++++++ cura/PrinterOutput/PrinterOutputDevice.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutput/Models/PrinterConfigurationModel.py b/cura/PrinterOutput/Models/PrinterConfigurationModel.py index 47b9532080..52c7b6f960 100644 --- a/cura/PrinterOutput/Models/PrinterConfigurationModel.py +++ b/cura/PrinterOutput/Models/PrinterConfigurationModel.py @@ -58,6 +58,14 @@ class PrinterConfigurationModel(QObject): return False return self._printer_type != "" + def hasAnyMaterialLoaded(self) -> bool: + if not self.isValid(): + return False + for configuration in self._extruder_configurations: + if configuration.activeMaterial and configuration.activeMaterial.type != "empty": + return True + return False + def __str__(self): message_chunks = [] message_chunks.append("Printer type: " + self._printer_type) diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index bb4f9e79fb..31daacbccc 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -222,7 +222,7 @@ class PrinterOutputDevice(QObject, OutputDevice): def _updateUniqueConfigurations(self) -> None: all_configurations = set() for printer in self._printers: - if printer.printerConfiguration is not None: + if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded(): all_configurations.add(printer.printerConfiguration) all_configurations.update(printer.availableConfigurations) new_configurations = sorted(all_configurations, key = lambda config: config.printerType) From 73b423138adc1b9f6c3763498c37ba700f1476a6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 16:35:15 +0200 Subject: [PATCH 5/7] Add a test to ensure empty configurations are not shown in the list --- .../PrinterOutput/TestPrinterOutputDevice.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/PrinterOutput/TestPrinterOutputDevice.py b/tests/PrinterOutput/TestPrinterOutputDevice.py index e0415295c1..d690297009 100644 --- a/tests/PrinterOutput/TestPrinterOutputDevice.py +++ b/tests/PrinterOutput/TestPrinterOutputDevice.py @@ -3,6 +3,8 @@ from unittest.mock import MagicMock import pytest from unittest.mock import patch +from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel +from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice @@ -61,4 +63,19 @@ def test_uniqueConfigurations(printer_output_device): # Once the type of printer is set, it's active configuration counts as being set. # In that case, that should also be added to the list of available configurations printer.updateType("blarg!") - assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration] \ No newline at end of file + assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration] + + +def test_uniqueConfigurations_empty_is_filtered_out(printer_output_device): + printer = PrinterOutputModel(MagicMock()) + # Add a printer and fire the signal that ensures they get hooked up correctly. + printer_output_device._printers = [printer] + printer_output_device._onPrintersChanged() + + empty_material = MaterialOutputModel(guid = "", type = "empty", color = "empty", brand = "Generic", name = "Empty") + empty_left_extruder = ExtruderConfigurationModel(0) + empty_left_extruder.setMaterial(empty_material) + empty_right_extruder = ExtruderConfigurationModel(1) + empty_right_extruder.setMaterial(empty_material) + printer.printerConfiguration.setExtruderConfigurations([empty_left_extruder, empty_right_extruder]) + assert printer_output_device.uniqueConfiguration == [] From 882352c99dbe91cad989c9d01de004722e762c8d Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 17:03:39 +0200 Subject: [PATCH 6/7] Test fixes, not working yet --- cura/PrinterOutput/Models/ExtruderConfigurationModel.py | 4 ++-- tests/PrinterOutput/TestPrinterOutputDevice.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index 5b4cb5d6f5..04a3c95afd 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -25,7 +25,7 @@ class ExtruderConfigurationModel(QObject): return self._position def setMaterial(self, material: Optional[MaterialOutputModel]) -> None: - if self._hotend_id != material: + if self._material != material: self._material = material self.extruderConfigurationChanged.emit() @@ -33,7 +33,7 @@ class ExtruderConfigurationModel(QObject): def activeMaterial(self) -> Optional[MaterialOutputModel]: return self._material - @pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged) + @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged) def material(self) -> Optional[MaterialOutputModel]: return self._material diff --git a/tests/PrinterOutput/TestPrinterOutputDevice.py b/tests/PrinterOutput/TestPrinterOutputDevice.py index d690297009..7a9e4e2cc5 100644 --- a/tests/PrinterOutput/TestPrinterOutputDevice.py +++ b/tests/PrinterOutput/TestPrinterOutputDevice.py @@ -63,6 +63,12 @@ def test_uniqueConfigurations(printer_output_device): # Once the type of printer is set, it's active configuration counts as being set. # In that case, that should also be added to the list of available configurations printer.updateType("blarg!") + loaded_material = MaterialOutputModel(guid = "", type = "PLA", color = "Blue", brand = "Generic", name = "Blue PLA") + loaded_left_extruder = ExtruderConfigurationModel(0) + loaded_left_extruder.setMaterial(loaded_material) + loaded_right_extruder = ExtruderConfigurationModel(1) + loaded_right_extruder.setMaterial(loaded_material) + printer.printerConfiguration.setExtruderConfigurations([loaded_left_extruder, loaded_right_extruder]) assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration] @@ -72,10 +78,11 @@ def test_uniqueConfigurations_empty_is_filtered_out(printer_output_device): printer_output_device._printers = [printer] printer_output_device._onPrintersChanged() + printer.updateType("blarg!") empty_material = MaterialOutputModel(guid = "", type = "empty", color = "empty", brand = "Generic", name = "Empty") empty_left_extruder = ExtruderConfigurationModel(0) empty_left_extruder.setMaterial(empty_material) empty_right_extruder = ExtruderConfigurationModel(1) empty_right_extruder.setMaterial(empty_material) printer.printerConfiguration.setExtruderConfigurations([empty_left_extruder, empty_right_extruder]) - assert printer_output_device.uniqueConfiguration == [] + assert printer_output_device.uniqueConfigurations == [] From 63f94830377b485d1a973ca5484877602cf8f205 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 23 Aug 2019 17:15:08 +0200 Subject: [PATCH 7/7] Connect the config changed of the configuration to that of the output model --- cura/PrinterOutput/Models/PrinterOutputModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index b8bea999c7..a1a23201fb 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -50,7 +50,7 @@ class PrinterOutputModel(QObject): self._active_printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders] - + self._active_printer_configuration.configurationChanged.connect(self.configurationChanged) self._available_printer_configurations = [] # type: List[PrinterConfigurationModel] self._camera_url = QUrl() # type: QUrl