Merge pull request #3925 from Ultimaker/feature_send_material_profiles

Send material profiles via wifi
This commit is contained in:
Ian Paschal 2018-07-02 13:54:38 +02:00 committed by GitHub
commit 68e1b58c89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 15 deletions

View file

@ -214,7 +214,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished)
def postFormWithParts(self, target:str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
if self._manager is None:
self._createNetworkManager()
request = self._createEmptyRequest(target, content_type=None)

View file

@ -1288,7 +1288,7 @@ class MachineManager(QObject):
@pyqtSlot(str)
def switchPrinterType(self, machine_name: str) -> None:
# Don't switch if the user tries to change to the same type of printer
if self._global_container_stack is None or self.self.activeMachineDefinitionName == machine_name:
if self._global_container_stack is None or self.activeMachineDefinitionName == machine_name:
return
# Get the definition id corresponding to this machine name
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()

View file

@ -23,6 +23,7 @@ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.NetworkCamera import NetworkCamera
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
from .SendMaterialJob import SendMaterialJob
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtGui import QDesktopServices
@ -30,7 +31,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from time import time
from datetime import datetime
from typing import Optional, Dict, List
from typing import Optional, Dict, List, Set
import io #To create the correct buffers for sending data to the printer.
import json
@ -94,6 +95,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
self.writeStarted.emit(self)
self.sendMaterialProfiles()
#Formats supported by this application (file types that we can actually write).
if file_handler:
file_formats = file_handler.getSupportedFileTypesWrite()
@ -370,6 +373,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# Keep a list of all completed jobs so we know if something changed next time.
self._finished_jobs = finished_jobs
## Called when the connection to the cluster changes.
def connect(self) -> None:
super().connect()
self.sendMaterialProfiles()
def _update(self) -> None:
super()._update()
self.get("printers/", on_finished = self._onGetPrintersDataFinished)
@ -538,6 +546,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._active_printer = None
self.activePrinterChanged.emit()
## Sync the material profiles in Cura with the printer.
#
# This gets called when connecting to a printer as well as when sending a
# print.
def sendMaterialProfiles(self) -> None:
job = SendMaterialJob(device = self)
job.run()
def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]:
try:

View file

@ -0,0 +1,99 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json #To understand the list of materials from the printer reply.
import os #To walk over material files.
import os.path #To filter on material files.
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest #To listen to the reply from the printer.
from typing import Any, Dict, Set, TYPE_CHECKING
import urllib.parse #For getting material IDs from their file names.
from UM.Job import Job #The interface we're implementing.
from UM.Logger import Logger
from UM.MimeTypeDatabase import MimeTypeDatabase #To strip the extensions of the material profile files.
from UM.Resources import Resources
from UM.Settings.ContainerRegistry import ContainerRegistry #To find the GUIDs of materials.
from cura.CuraApplication import CuraApplication #For the resource types.
if TYPE_CHECKING:
from .ClusterUM3OutputDevice import ClusterUM3OutputDevice
## Asynchronous job to send material profiles to the printer.
#
# This way it won't freeze up the interface while sending those materials.
class SendMaterialJob(Job):
def __init__(self, device: "ClusterUM3OutputDevice"):
super().__init__()
self.device = device #type: ClusterUM3OutputDevice
def run(self) -> None:
self.device.get("materials/", on_finished = self.sendMissingMaterials)
def sendMissingMaterials(self, reply: QNetworkReply) -> None:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: #Got an error from the HTTP request.
Logger.log("e", "Couldn't request current material storage on printer. Not syncing materials.")
return
remote_materials_list = reply.readAll().data().decode("utf-8")
try:
remote_materials_list = json.loads(remote_materials_list)
except json.JSONDecodeError:
Logger.log("e", "Current material storage on printer was a corrupted reply.")
return
try:
remote_materials_by_guid = {material["guid"]: material for material in remote_materials_list} #Index by GUID.
except KeyError:
Logger.log("e", "Current material storage on printer was an invalid reply (missing GUIDs).")
return
container_registry = ContainerRegistry.getInstance()
local_materials_list = filter(lambda material: ("GUID" in material and "version" in material and "id" in material), container_registry.findContainersMetadata(type = "material"))
local_materials_by_guid = {material["GUID"]: material for material in local_materials_list if material["id"] == material["base_file"]}
for material in local_materials_list: #For each GUID get the material with the highest version number.
try:
if int(material["version"]) > local_materials_by_guid[material["GUID"]]["version"]:
local_materials_by_guid[material["GUID"]] = material
except ValueError:
Logger.log("e", "Material {material_id} has invalid version number {number}.".format(material_id = material["id"], number = material["version"]))
continue
materials_to_send = set() #type: Set[Dict[str, Any]]
for guid, material in local_materials_by_guid.items():
if guid not in remote_materials_by_guid:
materials_to_send.add(material["id"])
continue
try:
if int(material["version"]) > remote_materials_by_guid[guid]["version"]:
materials_to_send.add(material["id"])
continue
except KeyError:
Logger.log("e", "Current material storage on printer was an invalid reply (missing version).")
return
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer):
try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
except MimeTypeDatabase.MimeTypeNotFoundError:
continue #Not the sort of file we'd like to send then.
_, file_name = os.path.split(file_path)
material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name))
if material_id not in materials_to_send:
continue
parts = []
with open(file_path, "rb") as f:
parts.append(self.device.createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read()))
signature_file_path = file_path + ".sig"
if os.path.exists(signature_file_path):
_, signature_file_name = os.path.split(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()))
Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id))
self.device.postFormWithParts(target = "materials/", parts = parts, onFinished = self.sendingFinished)
def sendingFinished(self, reply: QNetworkReply):
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("e", "Received error code from printer when syncing material: {code}".format(code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)))
Logger.log("e", reply.readAll().data().decode("utf-8"))

View file

@ -333,9 +333,9 @@ class XmlMaterialProfile(InstanceContainer):
stream = io.BytesIO()
tree = ET.ElementTree(root)
# this makes sure that the XML header states encoding="utf-8"
tree.write(stream, encoding="utf-8", xml_declaration=True)
tree.write(stream, encoding = "utf-8", xml_declaration=True)
return stream.getvalue().decode('utf-8')
return stream.getvalue().decode("utf-8")
# Recursively resolve loading inherited files
def _resolveInheritance(self, file_name):
@ -351,7 +351,7 @@ class XmlMaterialProfile(InstanceContainer):
def _loadFile(self, file_name):
path = Resources.getPath(CuraApplication.getInstance().ResourceTypes.MaterialInstanceContainer, file_name + ".xml.fdm_material")
with open(path, encoding="utf-8") as f:
with open(path, encoding = "utf-8") as f:
contents = f.read()
self._inherited_files.append(path)
@ -565,7 +565,16 @@ class XmlMaterialProfile(InstanceContainer):
for entry in settings:
key = entry.get("key")
if key in self.__material_settings_setting_map:
common_setting_values[self.__material_settings_setting_map[key]] = entry.text
if key == "processing temperature graph": #This setting has no setting text but subtags.
graph_nodes = entry.iterfind("./um:point", self.__namespaces)
graph_points = []
for graph_node in graph_nodes:
flow = float(graph_node.get("flow"))
temperature = float(graph_node.get("temperature"))
graph_points.append([flow, temperature])
common_setting_values[self.__material_settings_setting_map[key]] = str(graph_points)
else:
common_setting_values[self.__material_settings_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
common_compatibility = self._parseCompatibleValue(entry.text)
@ -598,7 +607,16 @@ class XmlMaterialProfile(InstanceContainer):
for entry in settings:
key = entry.get("key")
if key in self.__material_settings_setting_map:
machine_setting_values[self.__material_settings_setting_map[key]] = entry.text
if key == "processing temperature graph": #This setting has no setting text but subtags.
graph_nodes = entry.iterfind("./um:point", self.__namespaces)
graph_points = []
for graph_node in graph_nodes:
flow = float(graph_node.get("flow"))
temperature = float(graph_node.get("temperature"))
graph_points.append([flow, temperature])
machine_setting_values[self.__material_settings_setting_map[key]] = str(graph_points)
else:
machine_setting_values[self.__material_settings_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
machine_compatibility = self._parseCompatibleValue(entry.text)
@ -716,7 +734,16 @@ class XmlMaterialProfile(InstanceContainer):
for entry in settings:
key = entry.get("key")
if key in self.__material_settings_setting_map:
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
if key == "processing temperature graph": #This setting has no setting text but subtags.
graph_nodes = entry.iterfind("./um:point", self.__namespaces)
graph_points = []
for graph_node in graph_nodes:
flow = float(graph_node.get("flow"))
temperature = float(graph_node.get("temperature"))
graph_points.append([flow, temperature])
hotend_setting_values[self.__material_settings_setting_map[key]] = str(graph_points)
else:
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
elif key in self.__unmapped_settings:
if key == "hardware compatible":
hotend_compatibility = self._parseCompatibleValue(entry.text)
@ -965,9 +992,21 @@ class XmlMaterialProfile(InstanceContainer):
def _addSettingElement(self, builder, instance):
key = instance.definition.key
if key in self.__material_settings_setting_map.values():
# Setting has a key in the stabndard namespace
# Setting has a key in the standard namespace
key = UM.Dictionary.findKey(self.__material_settings_setting_map, instance.definition.key)
tag_name = "setting"
if key == "processing temperature graph": #The Processing Temperature Graph has its own little structure that we need to implement separately.
builder.start(tag_name, {"key": key})
graph_str = str(instance.value)
graph = graph_str.replace("[", "").replace("]", "").split(", ") #Crude parsing of this list: Flatten the list by removing all brackets, then split on ", ". Safe to eval attacks though!
graph = [graph[i:i + 2] for i in range(0, len(graph) - 1, 2)] #Convert to 2D array.
for point in graph:
builder.start("point", {"flow": point[0], "temperature": point[1]})
builder.end("point")
builder.end(tag_name)
return
elif key not in self.__material_properties_setting_map.values() and key not in self.__material_metadata_setting_map.values():
# Setting is not in the standard namespace, and not a material property (eg diameter) or metadata (eg GUID)
tag_name = "cura:setting"

View file

@ -1,12 +1,13 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict
class XmlMaterialValidator():
## Makes sure that the required metadata is present for a material.
class XmlMaterialValidator:
## Makes sure that the required metadata is present for a material.
@classmethod
def validateMaterialMetaData(cls, validation_metadata):
def validateMaterialMetaData(cls, validation_metadata: Dict[str, Any]):
if validation_metadata.get("GUID") is None:
return "Missing GUID"