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
|
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from typing import Dict, TYPE_CHECKING, Set, List
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Job import Job
from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from ..Models.Http.ClusterMaterial import ClusterMaterial
from ..Models.LocalMaterial import LocalMaterial
from ..Messages.MaterialSyncMessage import MaterialSyncMessage
if TYPE_CHECKING:
from .LocalClusterOutputDevice import LocalClusterOutputDevice
class SendMaterialJob(Job):
"""Asynchronous job to send material profiles to the printer.
This way it won't freeze up the interface while sending those materials.
"""
def __init__(self, device: "LocalClusterOutputDevice") -> None:
super().__init__()
self.device = device # type: LocalClusterOutputDevice
def run(self) -> None:
"""Send the request to the printer and register a callback"""
self.device.getMaterials(on_finished = self._onGetMaterials)
def _onGetMaterials(self, materials: List[ClusterMaterial]) -> None:
"""Callback for when the remote materials were returned."""
remote_materials_by_guid = {material.guid: material for material in materials}
self._sendMissingMaterials(remote_materials_by_guid)
def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None:
"""Determine which materials should be updated and send them to the printer.
:param remote_materials_by_guid: The remote materials by GUID.
"""
local_materials_by_guid = self._getLocalMaterials()
if len(local_materials_by_guid) == 0:
Logger.log("d", "There are no local materials to synchronize with the printer.")
return
material_ids_to_send = self._determineMaterialsToSend(local_materials_by_guid, remote_materials_by_guid)
if len(material_ids_to_send) == 0:
Logger.log("d", "There are no remote materials to update.")
return
self._sendMaterials(material_ids_to_send)
@staticmethod
def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial],
remote_materials: Dict[str, ClusterMaterial]) -> Set[str]:
"""From the local and remote materials, determine which ones should be synchronized.
Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that
are newer in Cura.
:param local_materials: The local materials by GUID.
:param remote_materials: The remote materials by GUID.
"""
return {
local_material.id
for guid, local_material in local_materials.items()
if guid not in remote_materials.keys() or local_material.version > remote_materials[guid].version
}
def _sendMaterials(self, materials_to_send: Set[str]) -> None:
"""Send the materials to the printer.
The given materials will be loaded from disk en sent to to printer.
The given id's will be matched with filenames of the locally stored materials.
:param materials_to_send: A set with id's of materials that must be sent.
"""
container_registry = CuraApplication.getInstance().getContainerRegistry()
all_materials = container_registry.findInstanceContainersMetadata(type = "material")
all_base_files = {material["base_file"] for material in all_materials if "base_file" in material} # Filters out uniques by making it a set. Don't include files without base file (i.e. empty material).
for root_material_id in all_base_files:
if root_material_id not in materials_to_send:
# If the material does not have to be sent we skip it.
continue
file_path = container_registry.getContainerFilePathById(root_material_id)
if not file_path:
Logger.log("w", "Cannot get file path for material container [%s]", root_material_id)
continue
file_name = os.path.basename(file_path)
self._sendMaterialFile(file_path, file_name, root_material_id)
def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
"""Send a single material file to the printer.
Also add the material signature file if that is available.
:param file_path: The path of the material file.
:param file_name: The name of the material file.
:param material_id: The ID of the material in the file.
"""
parts = []
# Add the material file.
try:
with open(file_path, "rb") as f:
parts.append(self.device.createFormPart("name=\"file\"; filename=\"{file_name}\""
.format(file_name = file_name), f.read()))
except FileNotFoundError:
Logger.error("Unable to send material {material_id}, since it has been deleted in the meanwhile.".format(material_id = material_id))
return
# Add the material signature file if needed.
signature_file_path = "{}.sig".format(file_path)
if os.path.exists(signature_file_path):
signature_file_name = os.path.basename(signature_file_path)
with open(signature_file_path, "rb") as f:
parts.append(self.device.createFormPart("name=\"signature_file\"; filename=\"{file_name}\""
.format(file_name = signature_file_name), f.read()))
# FIXME: move form posting to API client
self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts,
on_finished = self._sendingFinished)
def _sendingFinished(self, reply: QNetworkReply) -> None:
"""Check a reply from an upload to the printer and log an error when the call failed"""
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("w", "Error while syncing material: %s", reply.errorString())
return
body = reply.readAll().data().decode('utf8')
if "not added" in body:
# For some reason the cluster returns a 200 sometimes even when syncing failed.
return
# Inform the user that materials have been synced. This message only shows itself when not already visible.
# Because of the guards above it is not shown when syncing failed (which is not always an actual problem).
MaterialSyncMessage(self.device).show()
@staticmethod
def _getLocalMaterials() -> Dict[str, LocalMaterial]:
"""Retrieves a list of local materials
Only the new newest version of the local materials is returned
:return: a dictionary of LocalMaterial objects by GUID
"""
result = {} # type: Dict[str, LocalMaterial]
all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material")
all_base_files = [material for material in all_materials if material["id"] == material.get("base_file")] # Don't send materials without base_file: The empty material doesn't need to be sent.
# Find the latest version of all material containers in the registry.
for material_metadata in all_base_files:
try:
# material version must be an int
material_metadata["version"] = int(material_metadata["version"])
# Create a new local material
local_material = LocalMaterial(**material_metadata)
local_material.id = material_metadata["id"]
if local_material.GUID not in result or \
local_material.GUID not in result or \
local_material.version > result[local_material.GUID].version:
result[local_material.GUID] = local_material
except KeyError:
Logger.logException("w", "Local material {} has missing values.".format(material_metadata["id"]))
except ValueError:
Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
except TypeError:
Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
return result
|