mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-16 03:07:53 -06:00
Merge pull request #3925 from Ultimaker/feature_send_material_profiles
Send material profiles via wifi
This commit is contained in:
commit
68e1b58c89
6 changed files with 170 additions and 15 deletions
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
99
plugins/UM3NetworkPrinting/SendMaterialJob.py
Normal file
99
plugins/UM3NetworkPrinting/SendMaterialJob.py
Normal 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"))
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue