Welcome to mirror list, hosted at ThFree Co, Russian Federation.

GlobalStack.py « Settings « cura - github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 282034c0eef15b3eb9cd83c23bd37ac0202f5189 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

from collections import defaultdict
import threading
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
import uuid

from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal

from UM.Decorators import deprecated, override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Util import parseBool

import cura.CuraApplication
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType

from . import Exceptions
from .CuraContainerStack import CuraContainerStack

if TYPE_CHECKING:
    from cura.Settings.ExtruderStack import ExtruderStack


class GlobalStack(CuraContainerStack):
    """Represents the Global or Machine stack and its related containers."""

    def __init__(self, container_id: str) -> None:
        super().__init__(container_id)

        self.setMetaDataEntry("type", "machine")  # For backward compatibility

        # TL;DR: If Cura is looking for printers that belong to the same group, it should use "group_id".
        # Each GlobalStack by default belongs to a group which is identified via "group_id". This group_id is used to
        # figure out which GlobalStacks are in the printer cluster for example without knowing the implementation
        # details such as the um_network_key or some other identifier that's used by the underlying device plugin.
        self.setMetaDataEntry("group_id", str(uuid.uuid4()))  # Assign a new GlobalStack to a unique group by default

        self._extruders = {}  # type: Dict[str, "ExtruderStack"]

        # This property is used to track which settings we are calculating the "resolve" for
        # and if so, to bypass the resolve to prevent an infinite recursion that would occur
        # if the resolve function tried to access the same property it is a resolve for.
        # Per thread we have our own resolving_settings, or strange things sometimes occur.
        self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names

        # Since the metadatachanged is defined in container stack, we can't use it here as a notifier for pyqt
        # properties. So we need to tie them together like this.
        self.metaDataChanged.connect(self.configuredConnectionTypesChanged)

        self.setDirty(False)

    extrudersChanged = pyqtSignal()
    configuredConnectionTypesChanged = pyqtSignal()

    @pyqtProperty("QVariantMap", notify = extrudersChanged)
    @deprecated("Please use extruderList instead.", "4.4")
    def extruders(self) -> Dict[str, "ExtruderStack"]:
        """Get the list of extruders of this stack.

        :return: The extruders registered with this stack.
        """

        return self._extruders

    @pyqtProperty("QVariantList", notify = extrudersChanged)
    def extruderList(self) -> List["ExtruderStack"]:
        result_tuple_list = sorted(list(self._extruders.items()), key=lambda x: int(x[0]))
        result_list = [item[1] for item in result_tuple_list]

        machine_extruder_count = self.getProperty("machine_extruder_count", "value")
        return result_list[:machine_extruder_count]

    @pyqtProperty(int, constant = True)
    def maxExtruderCount(self):
        return len(self.getMetaDataEntry("machine_extruder_trains"))

    @pyqtProperty(bool, notify=configuredConnectionTypesChanged)
    def supportsNetworkConnection(self):
        return self.getMetaDataEntry("supports_network_connection", False)

    @pyqtProperty(bool, constant = True)
    def supportsMaterialExport(self):
        """
        Whether the printer supports Cura's export format of material profiles.
        :return: ``True`` if it supports it, or ``False`` if not.
        """
        return self.getMetaDataEntry("supports_material_export", False)

    @classmethod
    def getLoadingPriority(cls) -> int:
        return 2

    @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
    def configuredConnectionTypes(self) -> List[int]:
        """The configured connection types can be used to find out if the global

        stack is configured to be connected with a printer, without having to
        know all the details as to how this is exactly done (and without
        actually setting the stack to be active).

        This data can then in turn also be used when the global stack is active;
        If we can't get a network connection, but it is configured to have one,
        we can display a different icon to indicate the difference.
        """
        # Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
        # But we do want them returned as a list of ints (so the rest of the code can directly compare)
        connection_types = self.getMetaDataEntry("connection_type", "").split(",")
        result = []
        for connection_type in connection_types:
            if connection_type != "":
                try:
                    result.append(int(connection_type))
                except ValueError:
                    # We got invalid data, probably a None.
                    pass
        return result

    # Returns a boolean indicating if this machine has a remote connection. A machine is considered as remotely
    # connected if its connection types contain one of the following values:
    #   - ConnectionType.NetworkConnection
    #   - ConnectionType.CloudConnection
    @pyqtProperty(bool, notify = configuredConnectionTypesChanged)
    def hasRemoteConnection(self) -> bool:
        has_remote_connection = False

        for connection_type in self.configuredConnectionTypes:
            has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
                                                         ConnectionType.CloudConnection.value]
        return has_remote_connection

    def addConfiguredConnectionType(self, connection_type: int) -> None:
        """:sa configuredConnectionTypes"""

        configured_connection_types = self.configuredConnectionTypes
        if connection_type not in configured_connection_types:
            # Store the values as a string.
            configured_connection_types.append(connection_type)
            self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))

    def removeConfiguredConnectionType(self, connection_type: int) -> None:
        """:sa configuredConnectionTypes"""

        configured_connection_types = self.configuredConnectionTypes
        if connection_type in configured_connection_types:
            # Store the values as a string.
            configured_connection_types.remove(connection_type)
            self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))

    @classmethod
    def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
        configuration_type = super().getConfigurationTypeFromSerialized(serialized)
        if configuration_type == "machine":
            return "machine_stack"
        return configuration_type

    def getIntentCategory(self) -> str:
        intent_category = "default"
        for extruder in self.extruderList:
            category = extruder.intent.getMetaDataEntry("intent_category", "default")
            if category != "default" and category != intent_category:
                intent_category = category
        return intent_category

    def getBuildplateName(self) -> Optional[str]:
        name = None
        if self.variant.getId() != "empty_variant":
            name = self.variant.getName()
        return name

    @pyqtProperty(str, constant = True)
    def preferred_output_file_formats(self) -> str:
        return self.getMetaDataEntry("file_formats")

    def addExtruder(self, extruder: ContainerStack) -> None:
        """Add an extruder to the list of extruders of this stack.

        :param extruder: The extruder to add.

        :raise Exceptions.TooManyExtrudersError: Raised when trying to add an extruder while we
            already have the maximum number of extruders.
        """

        position = extruder.getMetaDataEntry("position")
        if position is None:
            Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id)
            return

        if any(item.getId() == extruder.id for item in self._extruders.values()):
            Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self.getId())
            return

        self._extruders[position] = extruder
        self.extrudersChanged.emit()
        Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)

    @override(ContainerStack)
    def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
        """Overridden from ContainerStack

        This will return the value of the specified property for the specified setting,
        unless the property is "value" and that setting has a "resolve" function set.
        When a resolve is set, it will instead try and execute the resolve first and
        then fall back to the normal "value" property.

        :param key: The setting key to get the property of.
        :param property_name: The property to get the value of.

        :return: The value of the property for the specified setting, or None if not found.
        """

        if not self.definition.findDefinitions(key = key):
            return None

        if context:
            context.pushContainer(self)

        # Handle the "resolve" property.
        #TODO: Why the hell does this involve threading?
        # Answer: Because if multiple threads start resolving properties that have the same underlying properties that's
        # related, without taking a note of which thread a resolve paths belongs to, they can bump into each other and
        # generate unexpected behaviours.
        if self._shouldResolve(key, property_name, context):
            current_thread = threading.current_thread()
            self._resolving_settings[current_thread.name].add(key)
            resolve = super().getProperty(key, "resolve", context)
            self._resolving_settings[current_thread.name].remove(key)
            if resolve is not None:
                return resolve

        # Handle the "limit_to_extruder" property.
        limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
        if limit_to_extruder is not None:
            if limit_to_extruder == -1:
                limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
            limit_to_extruder = str(limit_to_extruder)
        if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
            if super().getProperty(key, "settable_per_extruder", context):
                result = self._extruders[str(limit_to_extruder)].getProperty(key, property_name, context)
                if result is not None:
                    if context:
                        context.popContainer()
                    return result
            else:
                Logger.log("e", "Setting {setting} has limit_to_extruder but is not settable per extruder!", setting = key)

        result = super().getProperty(key, property_name, context)
        if context:
            context.popContainer()
        return result

    @override(ContainerStack)
    def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
        """Overridden from ContainerStack

        This will simply raise an exception since the Global stack cannot have a next stack.
        """

        raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")

    # Determine whether or not we should try to get the "resolve" property instead of the
    # requested property.
    def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
        if property_name != "value":
            # Do not try to resolve anything but the "value" property
            return False

        if not self.definition.getProperty(key, "resolve"):
            # If there isn't a resolve set for this setting, there isn't anything to do here.
            return False

        current_thread = threading.current_thread()
        if key in self._resolving_settings[current_thread.name]:
            # To prevent infinite recursion, if getProperty is called with the same key as
            # we are already trying to resolve, we should not try to resolve again. Since
            # this can happen multiple times when trying to resolve a value, we need to
            # track all settings that are being resolved.
            return False

        if self.hasUserValue(key):
            # When the user has explicitly set a value, we should ignore any resolve and just return that value.
            return False

        return True

    def isValid(self) -> bool:
        """Perform some sanity checks on the global stack

        Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
        """
        container_registry = ContainerRegistry.getInstance()
        extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId())

        machine_extruder_count = self.getProperty("machine_extruder_count", "value")
        extruder_check_position = set()
        for extruder_train in extruder_trains:
            extruder_position = extruder_train.getMetaDataEntry("position")
            extruder_check_position.add(extruder_position)

        for check_position in range(machine_extruder_count):
            if str(check_position) not in extruder_check_position:
                return False
        return True

    def getHeadAndFansCoordinates(self):
        return self.getProperty("machine_head_with_fans_polygon", "value")

    @pyqtProperty(bool, constant = True)
    def hasMaterials(self) -> bool:
        return parseBool(self.getMetaDataEntry("has_materials", False))

    @pyqtProperty(bool, constant = True)
    def hasVariants(self) -> bool:
        return parseBool(self.getMetaDataEntry("has_variants", False))

    @pyqtProperty(bool, constant = True)
    def hasVariantBuildplates(self) -> bool:
        return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))

    @pyqtSlot(result = str)
    def getDefaultFirmwareName(self) -> str:
        """Get default firmware file name if one is specified in the firmware"""

        machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")

        baudrate = 250000
        if Platform.isLinux():
            # Linux prefers a baudrate of 115200 here because older versions of
            # pySerial did not support a baudrate of 250000
            baudrate = 115200

        # If a firmware file is available, it should be specified in the definition for the printer
        hex_file = self.getMetaDataEntry("firmware_file", None)
        if machine_has_heated_bed:
            hex_file = self.getMetaDataEntry("firmware_hbk_file", hex_file)

        if not hex_file:
            Logger.log("w", "There is no firmware for machine %s.", self.getBottom().id)
            return ""

        try:
            return Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
        except FileNotFoundError:
            Logger.log("w", "Firmware file %s not found.", hex_file)
            return ""

    def getName(self) -> str:
        return self._metadata.get("group_name", self._metadata.get("name", ""))

    def setName(self, name: "str") -> None:
        super().setName(name)

    nameChanged = pyqtSignal()
    name = pyqtProperty(str, fget=getName, fset=setName, notify=nameChanged)



## private:
global_stack_mime = MimeType(
    name = "application/x-cura-globalstack",
    comment = "Cura Global Stack",
    suffixes = ["global.cfg"]
)

MimeTypeDatabase.addMimeType(global_stack_mime)
ContainerRegistry.addContainerTypeByName(GlobalStack, "global_stack", global_stack_mime.name)