Merge branch 'master' of https://github.com/Ultimaker/Cura into layerview_dev

This commit is contained in:
Johan K 2016-07-12 00:37:27 +02:00
commit bbd49cee85
80 changed files with 3683 additions and 1250 deletions

View file

@ -53,7 +53,6 @@ class ThreeMFReader(MeshReader):
triangles = entry.findall(".//3mf:triangle", self._namespaces)
mesh_builder.reserveFaceCount(len(triangles))
#for triangle in object.mesh.triangles.triangle:
for triangle in triangles:
v1 = int(triangle.get("v1"))
v2 = int(triangle.get("v2"))
@ -67,11 +66,11 @@ class ThreeMFReader(MeshReader):
# Rotate the model; We use a different coordinate frame.
rotation = Matrix()
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1,0,0))
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
#TODO: We currently do not check for normals and simply recalculate them.
# TODO: We currently do not check for normals and simply recalculate them.
mesh_builder.calculateNormals()
mesh_builder.setFileName(file_name)
node.setMeshData(mesh_builder.build().getTransformed(rotation))
node.setSelectable(True)
@ -108,11 +107,11 @@ class ThreeMFReader(MeshReader):
Job.yieldThread()
#If there is more then one object, group them.
# If there is more then one object, group them.
if len(objects) > 1:
group_decorator = GroupDecorator()
result.addDecorator(group_decorator)
except Exception as e:
Logger.log("e" ,"exception occured in 3mf reader: %s" , e)
Logger.log("e", "exception occured in 3mf reader: %s", e)
return result

View file

@ -82,10 +82,15 @@ message GCodeLayer {
bytes data = 2;
}
message ObjectPrintTime { // The print time for the whole print and material estimates for the first extruder
message PrintTimeMaterialEstimates { // The print time for the whole print and material estimates for the extruder
float time = 1; // Total time estimate
repeated MaterialEstimates materialEstimates = 2; // materialEstimates data
}
message MaterialEstimates {
int64 id = 1;
float time = 2; // Total time estimate
float material_amount = 3; // material used in the first extruder
float material_amount = 2; // material used in the extruder
}
message SettingList {

View file

@ -13,7 +13,7 @@ from UM.Resources import Resources
from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
from UM.Platform import Platform
from cura.ExtruderManager import ExtruderManager
import cura.Settings
from cura.OneAtATimeIterator import OneAtATimeIterator
from . import ProcessSlicedLayersJob
@ -64,7 +64,7 @@ class CuraEngineBackend(Backend):
self._onGlobalStackChanged()
self._active_extruder_stack = None
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
cura.Settings.ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
self._onActiveExtruderChanged()
#When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
@ -81,7 +81,7 @@ class CuraEngineBackend(Backend):
self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
self._message_handlers["cura.proto.ObjectPrintTime"] = self._onObjectPrintTimeMessage
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
self._start_slice_job = None
@ -128,12 +128,16 @@ class CuraEngineBackend(Backend):
## Perform a slice of the scene.
def slice(self):
if not self._enabled or not self._global_container_stack: #We shouldn't be slicing.
# try again in a short time
self._change_timer.start()
return
self.printDurationMessage.emit(0, [0])
self._stored_layer_data = []
self._stored_optimized_layer_data = []
if not self._enabled or not self._global_container_stack: #We shouldn't be slicing.
return
if self._slicing: #We were already slicing. Stop the old job.
self._terminate()
@ -304,9 +308,12 @@ class CuraEngineBackend(Backend):
## Called when a print time message is received from the engine.
#
# \param message The protobuf message containing the print time and
# material amount.
def _onObjectPrintTimeMessage(self, message):
self.printDurationMessage.emit(message.time, message.material_amount)
# material amount per extruder
def _onPrintTimeMaterialEstimates(self, message):
material_amounts = []
for index in range(message.repeatedMessageCount("materialEstimates")):
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
self.printDurationMessage.emit(message.time, material_amounts)
## Creates a new socket connection.
def _createSocket(self):
@ -389,8 +396,8 @@ class CuraEngineBackend(Backend):
self._active_extruder_stack.propertyChanged.disconnect(self._onSettingChanged)
self._active_extruder_stack.containersChanged.disconnect(self._onChanged)
self._active_extruder_stack = ExtruderManager.getInstance().getActiveExtruderStack()
self._active_extruder_stack = cura.Settings.ExtruderManager.getInstance().getActiveExtruderStack()
if self._active_extruder_stack:
self._active_extruder_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
self._active_extruder_stack.containersChanged.connect(self._onChanged)
self._onChanged()

View file

@ -15,7 +15,8 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Validator import ValidatorState
from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.ExtruderManager import ExtruderManager
import cura.Settings
class StartJobResult(IntEnum):
Finished = 1
@ -128,7 +129,7 @@ class StartSliceJob(Job):
self._buildGlobalSettingsMessage(stack)
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getBottom().getId()):
for extruder_stack in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(stack.getBottom().getId()):
self._buildExtruderMessage(extruder_stack)
for group in object_groups:
@ -208,4 +209,4 @@ class StartSliceJob(Job):
setting = message.addRepeatedMessage("settings")
setting.name = key
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
Job.yieldThread()
Job.yieldThread()

View file

@ -22,7 +22,7 @@ class GCodeProfileReader(ProfileReader):
# It can only read settings with the same version as the version it was
# written with. If the file format is changed in a way that breaks reverse
# compatibility, increment this version number!
version = 1
version = 2
## Dictionary that defines how characters are escaped when embedded in
# g-code.
@ -73,18 +73,14 @@ class GCodeProfileReader(ProfileReader):
serialized = pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], serialized)
Logger.log("i", "Serialized the following from %s: %s" %(file_name, repr(serialized)))
# Create an empty profile - the id will be changed later
# Create an empty profile - the id and name will be changed by the ContainerRegistry
profile = InstanceContainer("")
profile.addMetaDataEntry("type", "quality")
try:
profile.deserialize(serialized)
except Exception as e: # Not a valid g-code file.
Logger.log("e", "Unable to serialise the profile: %s", str(e))
return None
#Creating a unique name using the filename of the GCode
new_name = catalog.i18nc("@label", "Custom profile (%s)") %(os.path.splitext(os.path.basename(file_name))[0])
profile.setName(new_name)
profile._id = new_name
profile.addMetaDataEntry("type", "quality")
return profile

View file

@ -23,7 +23,7 @@ class GCodeWriter(MeshWriter):
# It can only read settings with the same version as the version it was
# written with. If the file format is changed in a way that breaks reverse
# compatibility, increment this version number!
version = 1
version = 2
## Dictionary that defines how characters are escaped when embedded in
# g-code.
@ -68,23 +68,21 @@ class GCodeWriter(MeshWriter):
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
prefix_length = len(prefix)
all_settings = InstanceContainer("G-code-imported-profile") #Create a new 'profile' with ALL settings so that the slice can be precisely reproduced.
all_settings.setDefinition(settings.getBottom())
for key in settings.getAllKeys():
all_settings.setProperty(key, "value", settings.getProperty(key, "value")) #Just copy everything over to the setting instance.
serialised = all_settings.serialize()
global_stack = Application.getInstance().getGlobalContainerStack()
container_with_profile = global_stack.findContainer({"type": "quality"})
serialized = container_with_profile.serialize()
# Escape characters that have a special meaning in g-code comments.
pattern = re.compile("|".join(GCodeWriter.escape_characters.keys()))
# Perform the replacement with a regular expression.
serialised = pattern.sub(lambda m: GCodeWriter.escape_characters[re.escape(m.group(0))], serialised)
serialized = pattern.sub(lambda m: GCodeWriter.escape_characters[re.escape(m.group(0))], serialized)
# Introduce line breaks so that each comment is no longer than 80 characters. Prepend each line with the prefix.
result = ""
# Lines have 80 characters, so the payload of each line is 80 - prefix.
for pos in range(0, len(serialised), 80 - prefix_length):
result += prefix + serialised[pos : pos + 80 - prefix_length] + "\n"
serialised = result
for pos in range(0, len(serialized), 80 - prefix_length):
result += prefix + serialized[pos : pos + 80 - prefix_length] + "\n"
serialized = result
return serialised
return serialized

View file

@ -137,8 +137,6 @@ class LayerView(View):
self.currentLayerNumChanged.emit()
currentLayerNumChanged = Signal()
def calculateMaxLayers(self):
scene = self.getController().getScene()
renderer = self.getRenderer() # TODO: @UnusedVariable

View file

@ -54,9 +54,13 @@ Item
horizontalAlignment: TextInput.AlignRight;
onEditingFinished:
{
// Ensure that the cursor is at the first position. On some systems the text isn't fully visible
// Seems to have to do something with different dpi densities that QML doesn't quite handle.
// Another option would be to increase the size even further, but that gives pretty ugly results.
cursorPosition = 0;
if(valueLabel.text != '')
{
slider.value = valueLabel.text - 1
slider.value = valueLabel.text - 1;
}
}
validator: IntValidator { bottom: 1; top: slider.maximumValue + 1; }
@ -66,10 +70,6 @@ Item
anchors.verticalCenter: parent.verticalCenter;
width: Math.max(UM.Theme.getSize("line").width * maxValue.length + 2, 20);
// Ensure that the cursor is at the first position. On some systems the text isnt fully visible
// Seems to have to do something with different dpi densities that QML doesn't quite handle.
// Another option would be to increase the size even further, but that gives pretty ugly results.
onTextChanged: cursorPosition = 0
style: TextFieldStyle
{
textColor: UM.Theme.getColor("setting_control_text");

View file

@ -5,7 +5,7 @@ from UM.Logger import Logger
import UM.Settings.Models
from cura.SettingOverrideDecorator import SettingOverrideDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## The per object setting visibility handler ensures that only setting defintions that have a matching instance Container
# are returned as visible.

View file

@ -5,7 +5,7 @@ from UM.Tool import Tool
from UM.Scene.Selection import Selection
from UM.Application import Application
from UM.Preferences import Preferences
from cura.SettingOverrideDecorator import SettingOverrideDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## This tool allows the user to add & change settings per node in the scene.
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.

View file

@ -9,6 +9,7 @@ from UM.Scene.SceneNode import SceneNode
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Platform import Platform
import collections
import json
@ -18,6 +19,7 @@ import platform
import math
import urllib.request
import urllib.parse
import ssl
catalog = i18nCatalog("cura")
@ -45,72 +47,84 @@ class SliceInfo(Extension):
Preferences.getInstance().setValue("info/asked_send_slice_info", True)
def _onWriteStarted(self, output_device):
if not Preferences.getInstance().getValue("info/send_slice_info"):
Logger.log("d", "'info/send_slice_info' is turned off.")
return # Do nothing, user does not want to send data
global_container_stack = Application.getInstance().getGlobalContainerStack()
# Get total material used (in mm^3)
print_information = Application.getInstance().getPrintInformation()
material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")
material_used = math.pi * material_radius * material_radius * print_information.materialAmount #Volume of material used
# Get model information (bounding boxes, hashes and transformation matrix)
models_info = []
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False):
model_info = {}
model_info["hash"] = node.getMeshData().getHash()
model_info["bounding_box"] = {}
model_info["bounding_box"]["minimum"] = {}
model_info["bounding_box"]["minimum"]["x"] = node.getBoundingBox().minimum.x
model_info["bounding_box"]["minimum"]["y"] = node.getBoundingBox().minimum.y
model_info["bounding_box"]["minimum"]["z"] = node.getBoundingBox().minimum.z
model_info["bounding_box"]["maximum"] = {}
model_info["bounding_box"]["maximum"]["x"] = node.getBoundingBox().maximum.x
model_info["bounding_box"]["maximum"]["y"] = node.getBoundingBox().maximum.y
model_info["bounding_box"]["maximum"]["z"] = node.getBoundingBox().maximum.z
model_info["transformation"] = str(node.getWorldTransformation().getData())
models_info.append(model_info)
# Bundle the collected data
submitted_data = {
"processor": platform.processor(),
"machine": platform.machine(),
"platform": platform.platform(),
"settings": global_container_stack.serialize(), # global_container with references on used containers
"version": Application.getInstance().getVersion(),
"modelhash": "None",
"printtime": print_information.currentPrintTime.getDisplayString(),
"filament": material_used,
"language": Preferences.getInstance().getValue("general/language"),
"materials_profiles ": {}
}
for container in global_container_stack.getContainers():
container_id = container.getId()
try:
container_serialized = container.serialize()
except NotImplementedError:
Logger.log("w", "Container %s could not be serialized!", container_id)
continue
if container_serialized:
submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
else:
Logger.log("i", "No data found in %s to be serialized!", container_id)
# Convert data to bytes
submitted_data = urllib.parse.urlencode(submitted_data)
binary_data = submitted_data.encode("utf-8")
# Submit data
try:
f = urllib.request.urlopen(self.info_url, data = binary_data, timeout = 1)
Logger.log("i", "Sent anonymous slice info to %s", self.info_url)
f.close()
except Exception as e:
Logger.logException("e", e)
if not Preferences.getInstance().getValue("info/send_slice_info"):
Logger.log("d", "'info/send_slice_info' is turned off.")
return # Do nothing, user does not want to send data
global_container_stack = Application.getInstance().getGlobalContainerStack()
# Get total material used (in mm^3)
print_information = Application.getInstance().getPrintInformation()
material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")
# TODO: Send material per extruder instead of mashing it on a pile
material_used = math.pi * material_radius * material_radius * sum(print_information.materialAmounts) #Volume of all materials used
# Get model information (bounding boxes, hashes and transformation matrix)
models_info = []
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if not getattr(node, "_outside_buildarea", False):
model_info = {}
model_info["hash"] = node.getMeshData().getHash()
model_info["bounding_box"] = {}
model_info["bounding_box"]["minimum"] = {}
model_info["bounding_box"]["minimum"]["x"] = node.getBoundingBox().minimum.x
model_info["bounding_box"]["minimum"]["y"] = node.getBoundingBox().minimum.y
model_info["bounding_box"]["minimum"]["z"] = node.getBoundingBox().minimum.z
model_info["bounding_box"]["maximum"] = {}
model_info["bounding_box"]["maximum"]["x"] = node.getBoundingBox().maximum.x
model_info["bounding_box"]["maximum"]["y"] = node.getBoundingBox().maximum.y
model_info["bounding_box"]["maximum"]["z"] = node.getBoundingBox().maximum.z
model_info["transformation"] = str(node.getWorldTransformation().getData())
models_info.append(model_info)
# Bundle the collected data
submitted_data = {
"processor": platform.processor(),
"machine": platform.machine(),
"platform": platform.platform(),
"settings": global_container_stack.serialize(), # global_container with references on used containers
"version": Application.getInstance().getVersion(),
"modelhash": "None",
"printtime": print_information.currentPrintTime.getDisplayString(),
"filament": material_used,
"language": Preferences.getInstance().getValue("general/language"),
"materials_profiles ": {}
}
for container in global_container_stack.getContainers():
container_id = container.getId()
try:
container_serialized = container.serialize()
except NotImplementedError:
Logger.log("w", "Container %s could not be serialized!", container_id)
continue
if container_serialized:
submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
else:
Logger.log("i", "No data found in %s to be serialized!", container_id)
# Convert data to bytes
submitted_data = urllib.parse.urlencode(submitted_data)
binary_data = submitted_data.encode("utf-8")
# Submit data
kwoptions = {"data" : binary_data,
"timeout" : 1
}
if Platform.isOSX():
kwoptions["context"] = ssl._create_unverified_context()
try:
f = urllib.request.urlopen(self.info_url, **kwoptions)
Logger.log("i", "Sent anonymous slice info to %s", self.info_url)
f.close()
except Exception as e:
Logger.logException("e", "An exception occurred while trying to send slice information")
except:
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
pass

View file

@ -10,7 +10,7 @@ from UM.View.Renderer import Renderer
from UM.View.GL.OpenGL import OpenGL
from cura.ExtrudersModel import ExtrudersModel
import cura.Settings
import math
@ -24,7 +24,7 @@ class SolidView(View):
self._enabled_shader = None
self._disabled_shader = None
self._extruders_model = ExtrudersModel()
self._extruders_model = cura.Settings.ExtrudersModel()
def beginRendering(self):
scene = self.getController().getScene()

View file

@ -1,72 +0,0 @@
// Copyright (c) 2015 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.1 as UM
UM.Dialog
{
width: 500 * Screen.devicePixelRatio;
height: 100 * Screen.devicePixelRatio;
modality: Qt.NonModal
title: catalog.i18nc("@title:window", "Print via USB")
Column
{
anchors.fill: parent;
Row
{
spacing: UM.Theme.getSize("default_margin").width;
Label
{
//: USB Printing dialog label, %1 is head temperature
text: catalog.i18nc("@label","Extruder Temperature %1").arg(manager.hotendTemperatures[0])
}
Label
{
//: USB Printing dialog label, %1 is bed temperature
text: catalog.i18nc("@label","Bed Temperature %1").arg(manager.bedTemperature)
}
Label
{
text: "" + manager.error
}
UM.I18nCatalog{id: catalog; name:"cura"}
}
ProgressBar
{
id: prog;
anchors.left: parent.left;
anchors.right: parent.right;
minimumValue: 0;
maximumValue: 100;
value: manager.progress
}
}
rightButtons: [
Button
{
//: USB Printing dialog start print button
text: catalog.i18nc("@action:button","Print");
onClicked: { manager.startPrint() }
enabled: manager.progress == 0 ? true : false
},
Button
{
//: USB Printing dialog cancel print button
text: catalog.i18nc("@action:button","Cancel");
onClicked: { manager.cancelPrint() }
enabled: manager.progress == 0 ? false: true
}
]
}

View file

@ -26,8 +26,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def __init__(self, serial_port):
super().__init__(serial_port)
self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
self.setShortDescription(catalog.i18nc("@action:button", "Print with USB"))
self.setDescription(catalog.i18nc("@info:tooltip", "Print with USB"))
self.setShortDescription(catalog.i18nc("@action:button", "Print via USB"))
self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB"))
self.setIconName("print")
self._serial = None
@ -37,9 +37,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._connect_thread = threading.Thread(target = self._connect)
self._connect_thread.daemon = True
self._end_stop_thread = threading.Thread(target = self._pollEndStop)
self._end_stop_thread.daemon = True
self._poll_endstop = -1
self._end_stop_thread = None
self._poll_endstop = False
# The baud checking is done by sending a number of m105 commands to the printer and waiting for a readable
# response. If the baudrate is correct, this should make sense, else we get giberish.
@ -51,13 +50,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._update_firmware_thread = threading.Thread(target= self._updateFirmware)
self._update_firmware_thread.daemon = True
self.firmwareUpdateComplete.connect(self._onFirmwareUpdateComplete)
self._heatup_wait_start_time = time.time()
## Queue for commands that need to be send. Used when command is sent when a print is active.
self._command_queue = queue.Queue()
self._is_printing = False
self._is_paused = False
## Set when print is started in order to check running time.
self._print_start_time = None
@ -80,13 +80,15 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# In order to keep the connection alive we request the temperature every so often from a different extruder.
# This index is the extruder we requested data from the last time.
self._temperature_requested_extruder_index = 0
self._temperature_requested_extruder_index = 0
self._current_z = 0
self._updating_firmware = False
self._firmware_file_name = None
self._control_view = None
self._error_message = None
onError = pyqtSignal()
@ -120,10 +122,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def _homeBed(self):
self._sendCommand("G28 Z")
@pyqtSlot()
def startPrint(self):
self.writeStarted.emit(self)
gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list")
self._updateJobState("printing")
self.printGCode(gcode_list)
def _moveHead(self, x, y, z, speed):
@ -135,6 +137,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# \param gcode_list List with gcode (strings).
def printGCode(self, gcode_list):
if self._progress or self._connection_state != ConnectionState.connected:
self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is busy or not connected. Unable to start a new job."))
self._error_message.show()
Logger.log("d", "Printer is busy or not connected, aborting print")
self.writeError.emit(self)
return
@ -216,13 +220,17 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
@pyqtSlot()
def startPollEndstop(self):
if self._poll_endstop == -1:
if not self._poll_endstop:
self._poll_endstop = True
if self._end_stop_thread is None:
self._end_stop_thread = threading.Thread(target=self._pollEndStop)
self._end_stop_thread.daemon = True
self._end_stop_thread.start()
@pyqtSlot()
def stopPollEndstop(self):
self._poll_endstop = False
self._end_stop_thread = None
def _pollEndStop(self):
while self._connection_state == ConnectionState.connected and self._poll_endstop:
@ -344,23 +352,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._setErrorState("Unexpected error while writing serial port %s " % e)
self.close()
def createControlInterface(self):
if self._control_view is None:
Logger.log("d", "Creating control interface for printer connection")
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "ControlWindow.qml"))
component = QQmlComponent(Application.getInstance()._engine, path)
self._control_context = QQmlContext(Application.getInstance()._engine.rootContext())
self._control_context.setContextProperty("manager", self)
self._control_view = component.create(self._control_context)
## Show control interface.
# This will create the view if its not already created.
def showControlInterface(self):
if self._control_view is None:
self.createControlInterface()
self._control_view.show()
## Send a command to printer.
## Send a command to printer.
# \param cmd string with g-code
def sendCommand(self, cmd):
if self._progress:
@ -371,11 +363,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Set the error state with a message.
# \param error String with the error message.
def _setErrorState(self, error):
self._updateJobState("error")
self._error_state = error
self.onError.emit()
def requestWrite(self, node, file_name = None, filter_by_machine = False):
self.showControlInterface()
Application.getInstance().showPrintMonitor.emit(True)
self.startPrint()
def _setEndstopState(self, endstop_key, value):
if endstop_key == b"x_min":
@ -391,14 +385,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self.endstopStateChanged.emit("z_min", value)
self._z_min_endstop_pressed = value
## Listen thread function.
## Listen thread function.
def _listen(self):
Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port)
temperature_request_timeout = time.time()
ok_timeout = time.time()
while self._connection_state == ConnectionState.connected:
line = self._readline()
if line is None:
if line is None:
break # None is only returned when something went wrong. Stop listening
if time.time() > temperature_request_timeout:
@ -423,7 +417,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._setErrorState(line[6:])
elif b" T:" in line or line.startswith(b"T:"): # Temperature message
try:
try:
self._setHotendTemperature(self._temperature_requested_extruder_index, float(re.search(b"T: *([0-9\.]*)", line).group(1)))
except:
pass
@ -445,6 +439,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
ok_timeout = time.time() + 5
if not self._command_queue.empty():
self._sendCommand(self._command_queue.get())
elif self._is_paused:
line = b"" # Force getting temperature as keep alive
else:
self._sendNextGcodeLine()
elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with "resend" and "rs"
@ -454,13 +450,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if b"rs" in line:
self._gcode_position = int(line.split()[1])
else: # Request the temperature on comm timeout (every 2 seconds) when we are not printing.)
if line == b"":
if self._num_extruders > 0:
self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders
self.sendCommand("M105 T%d" % self._temperature_requested_extruder_index)
else:
self.sendCommand("M105")
# Request the temperature on comm timeout (every 2 seconds) when we are not printing.)
if line == b"":
if self._num_extruders > 0:
self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders
self.sendCommand("M105 T%d" % self._temperature_requested_extruder_index)
else:
self.sendCommand("M105")
Logger.log("i", "Printer connection listen thread stopped for %s" % self._serial_port)
## Send next Gcode in the gcode list
@ -487,10 +484,22 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line)))
self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
self._gcode_position += 1
self._gcode_position += 1
self.setProgress((self._gcode_position / len(self._gcode)) * 100)
self.progressChanged.emit()
## Set the state of the print.
# Sent from the print monitor
def _setJobState(self, job_state):
if job_state == "pause":
self._is_paused = True
self._updateJobState("paused")
elif job_state == "print":
self._is_paused = False
self._updateJobState("printing")
elif job_state == "abort":
self.cancelPrint()
## Set the progress of the print.
# It will be normalized (based on max_progress) to range 0 - 100
def setProgress(self, progress, max_progress = 100):
@ -498,16 +507,20 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self.progressChanged.emit()
## Cancel the current print. Printer connection wil continue to listen.
@pyqtSlot()
def cancelPrint(self):
self._gcode_position = 0
self.setProgress(0)
self._gcode = []
# Turn of temperatures
# Turn off temperatures, fan and steppers
self._sendCommand("M140 S0")
self._sendCommand("M104 S0")
self._sendCommand("M107")
self._sendCommand("M84")
self._is_printing = False
self._is_paused = False
self._updateJobState("ready")
Application.getInstance().showPrintMonitor.emit(False)
## Check if the process did not encounter an error yet.
def hasError(self):

View file

@ -41,10 +41,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
self._check_updates = True
self._firmware_view = None
## Add menu item to top menu of the application.
self.setMenuName(i18n_catalog.i18nc("@title:menu","Firmware"))
self.addMenuItem(i18n_catalog.i18nc("@item:inmenu", "Update Firmware"), self.updateAllFirmware)
Application.getInstance().applicationShuttingDown.connect(self.stop)
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
@ -156,7 +152,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
"ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex",
"ultimaker2" : "MarlinUltimaker2.hex",
"ultimaker2_go" : "MarlinUltimaker2go.hex",
"ultimaker2plus" : "MarlinUltimaker2plus.hex",
"ultimaker2_plus" : "MarlinUltimaker2plus.hex",
"ultimaker2_extended" : "MarlinUltimaker2extended.hex",
"ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex",
}

View file

@ -1,14 +1,16 @@
from cura.MachineAction import MachineAction
from cura.PrinterOutputDevice import PrinterOutputDevice
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import pyqtSlot
from UM.Application import Application
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.PrinterOutputDevice import PrinterOutputDevice
class BedLevelMachineAction(MachineAction):
def __init__(self):
super().__init__("BedLevel", "Level bed")
super().__init__("BedLevel", catalog.i18nc("@action", "Level bed"))
self._qml_url = "BedLevelMachineAction.qml"
self._bed_level_position = 0

View file

@ -3,9 +3,14 @@ from cura.PrinterOutputDevice import PrinterOutputDevice
from UM.Application import Application
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UMOCheckupMachineAction(MachineAction):
def __init__(self):
super().__init__("UMOCheckup", "Checkup")
super().__init__("UMOCheckup", catalog.i18nc("@action", "Checkup"))
self._qml_url = "UMOCheckupMachineAction.qml"
self._hotend_target_temp = 180
self._bed_target_temp = 60
@ -39,7 +44,6 @@ class UMOCheckupMachineAction(MachineAction):
if self._output_device is None and self._check_started:
self.startCheck()
def _getPrinterOutputDevices(self):
return [printer_output_device for printer_output_device in
Application.getInstance().getOutputDeviceManager().getOutputDevices() if
@ -54,11 +58,13 @@ class UMOCheckupMachineAction(MachineAction):
self._output_device.endstopStateChanged.disconnect(self._onEndstopStateChanged)
try:
self._output_device.stopPollEndstop()
except AttributeError: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
pass
except AttributeError as e: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
Logger.log("e", "An exception occurred while stopping end stop polling: %s" % str(e))
self._output_device = None
self._check_started = False
self.checkStartedChanged.emit()
# Ensure everything is reset (and right signals are emitted again)
self._bed_test_completed = False
@ -73,6 +79,8 @@ class UMOCheckupMachineAction(MachineAction):
self._z_min_endstop_test_completed = False
self.onZMinEndstopTestCompleted.emit()
self.heatedBedChanged.emit()
@pyqtProperty(bool, notify = onBedTestCompleted)
def bedTestCompleted(self):
return self._bed_test_completed
@ -133,9 +141,16 @@ class UMOCheckupMachineAction(MachineAction):
self._z_min_endstop_test_completed = True
self.onZMinEndstopTestCompleted.emit()
checkStartedChanged = pyqtSignal()
@pyqtProperty(bool, notify = checkStartedChanged)
def checkStarted(self):
return self._check_started
@pyqtSlot()
def startCheck(self):
self._check_started = True
self.checkStartedChanged.emit()
output_devices = self._getPrinterOutputDevices()
if output_devices:
self._output_device = output_devices[0]
@ -146,8 +161,18 @@ class UMOCheckupMachineAction(MachineAction):
self._output_device.bedTemperatureChanged.connect(self._onBedTemperatureChanged)
self._output_device.hotendTemperaturesChanged.connect(self._onHotendTemperatureChanged)
self._output_device.endstopStateChanged.connect(self._onEndstopStateChanged)
except AttributeError: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
pass
except AttributeError as e: # Connection is probably not a USB connection. Something went pretty wrong if this happens.
Logger.log("e", "An exception occurred while starting end stop polling: %s" % str(e))
@pyqtSlot()
def cooldownHotend(self):
if self._output_device is not None:
self._output_device.setTargetHotendTemperature(0, 0)
@pyqtSlot()
def cooldownBed(self):
if self._output_device is not None:
self._output_device.setTargetBedTemperature(0)
@pyqtSlot()
def heatupHotend(self):
@ -157,4 +182,11 @@ class UMOCheckupMachineAction(MachineAction):
@pyqtSlot()
def heatupBed(self):
if self._output_device is not None:
self._output_device.setTargetBedTemperature(self._bed_target_temp)
self._output_device.setTargetBedTemperature(self._bed_target_temp)
heatedBedChanged = pyqtSignal()
@pyqtProperty(bool, notify = heatedBedChanged)
def hasHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
return global_container_stack.getProperty("machine_heated_bed", "value")

View file

@ -15,6 +15,10 @@ Cura.MachineAction
anchors.fill: parent;
property int leftRow: checkupMachineAction.width * 0.40
property int rightRow: checkupMachineAction.width * 0.60
property bool heatupHotendStarted: false
property bool heatupBedStarted: false
property bool usbConnected: Cura.USBPrinterManager.connectedPrinterList.rowCount() > 0
UM.I18nCatalog { id: catalog; name:"cura"}
Label
{
@ -32,7 +36,7 @@ Cura.MachineAction
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","It's a good idea to do a few sanity checks on your Ultimaker. You can skip this step if you know your machine is functional");
text: catalog.i18nc("@label", "It's a good idea to do a few sanity checks on your Ultimaker. You can skip this step if you know your machine is functional");
}
Item
@ -51,8 +55,8 @@ Cura.MachineAction
text: catalog.i18nc("@action:button","Start Printer Check");
onClicked:
{
checkupContent.visible = true
startCheckButton.enabled = false
checkupMachineAction.heatupHotendStarted = false
checkupMachineAction.heatupBedStarted = false
manager.startCheck()
}
}
@ -74,7 +78,7 @@ Cura.MachineAction
id: checkupContent
anchors.top: startStopButtons.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
visible: false
visible: manager.checkStarted
width: parent.width
height: 250
//////////////////////////////////////////////////////////
@ -94,7 +98,7 @@ Cura.MachineAction
anchors.left: connectionLabel.right
anchors.top: parent.top
wrapMode: Text.WordWrap
text: Cura.USBPrinterManager.connectedPrinterList.rowCount() > 0 || base.addOriginalProgress.checkUp[0] ? catalog.i18nc("@info:status","Done"):catalog.i18nc("@info:status","Incomplete")
text: checkupMachineAction.usbConnected ? catalog.i18nc("@info:status","Connected"): catalog.i18nc("@info:status","Not connected")
}
//////////////////////////////////////////////////////////
Label
@ -105,6 +109,7 @@ Cura.MachineAction
anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop X: ")
visible: checkupMachineAction.usbConnected
}
Label
{
@ -114,6 +119,7 @@ Cura.MachineAction
anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap
text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
//////////////////////////////////////////////////////////////
Label
@ -124,6 +130,7 @@ Cura.MachineAction
anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Y: ")
visible: checkupMachineAction.usbConnected
}
Label
{
@ -133,6 +140,7 @@ Cura.MachineAction
anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap
text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
/////////////////////////////////////////////////////////////////////
Label
@ -143,6 +151,7 @@ Cura.MachineAction
anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Z: ")
visible: checkupMachineAction.usbConnected
}
Label
{
@ -152,16 +161,19 @@ Cura.MachineAction
anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap
text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
////////////////////////////////////////////////////////////
Label
{
id: nozzleTempLabel
width: checkupMachineAction.leftRow
height: nozzleTempButton.height
anchors.left: parent.left
anchors.top: endstopZLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Nozzle temperature check: ")
visible: checkupMachineAction.usbConnected
}
Label
{
@ -171,25 +183,32 @@ Cura.MachineAction
anchors.left: nozzleTempLabel.right
wrapMode: Text.WordWrap
text: catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected
}
Item
{
id: nozzleTempButton
width: checkupMachineAction.rightRow * 0.3
height: nozzleTemp.height
height: childrenRect.height
anchors.top: nozzleTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
visible: checkupMachineAction.usbConnected
Button
{
height: nozzleTemp.height - 2
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: catalog.i18nc("@action:button","Start Heating")
text: checkupMachineAction.heatupHotendStarted ? catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
//
onClicked:
{
manager.heatupHotend()
nozzleTempStatus.text = catalog.i18nc("@info:progress","Checking")
if (checkupMachineAction.heatupHotendStarted)
{
manager.cooldownHotend()
checkupMachineAction.heatupHotendStarted = false
} else
{
manager.heatupHotend()
checkupMachineAction.heatupHotendStarted = true
}
}
}
}
@ -203,16 +222,19 @@ Cura.MachineAction
wrapMode: Text.WordWrap
text: manager.hotendTemperature + "°C"
font.bold: true
visible: checkupMachineAction.usbConnected
}
/////////////////////////////////////////////////////////////////////////////
Label
{
id: bedTempLabel
width: checkupMachineAction.leftRow
height: bedTempButton.height
anchors.left: parent.left
anchors.top: nozzleTempLabel.bottom
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","bed temperature check:")
text: catalog.i18nc("@label","Bed temperature check:")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
}
Label
@ -223,24 +245,31 @@ Cura.MachineAction
anchors.left: bedTempLabel.right
wrapMode: Text.WordWrap
text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
}
Item
{
id: bedTempButton
width: checkupMachineAction.rightRow * 0.3
height: bedTemp.height
height: childrenRect.height
anchors.top: bedTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
Button
{
height: bedTemp.height - 2
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: catalog.i18nc("@action:button","Start Heating")
text: checkupMachineAction.heatupBedStarted ?catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
onClicked:
{
manager.heatupBed()
if (checkupMachineAction.heatupBedStarted)
{
manager.cooldownBed()
checkupMachineAction.heatupBedStarted = false
} else
{
manager.heatupBed()
checkupMachineAction.heatupBedStarted = true
}
}
}
}
@ -254,6 +283,7 @@ Cura.MachineAction
wrapMode: Text.WordWrap
text: manager.bedTemperature + "°C"
font.bold: true
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
}
Label
{

View file

@ -0,0 +1,47 @@
from cura.MachineAction import MachineAction
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Application import Application
catalog = i18nCatalog("cura")
import UM.Settings.InstanceContainer
class UMOUpgradeSelection(MachineAction):
def __init__(self):
super().__init__("UMOUpgradeSelection", catalog.i18nc("@action", "Select upgrades"))
self._qml_url = "UMOUpgradeSelectionMachineAction.qml"
heatedBedChanged = pyqtSignal()
@pyqtProperty(bool, notify = heatedBedChanged)
def hasHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
return global_container_stack.getProperty("machine_heated_bed", "value")
@pyqtSlot()
def addHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant = global_container_stack.findContainer({"type": "variant"})
if variant:
if variant.getId() == "empty_variant":
variant_index = global_container_stack.getContainerIndex(variant)
stack_name = global_container_stack.getName()
new_variant = UM.Settings.InstanceContainer(stack_name + "_variant")
new_variant.addMetaDataEntry("type", "variant")
new_variant.setDefinition(global_container_stack.getBottom())
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
global_container_stack.replaceContainer(variant_index, new_variant)
variant = new_variant
variant.setProperty("machine_heated_bed", "value", True)
self.heatedBedChanged.emit()
@pyqtSlot()
def removeHeatedBed(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant = global_container_stack.findContainer({"type": "variant"})
if variant:
variant.setProperty("machine_heated_bed", "value", False)
self.heatedBedChanged.emit()

View file

@ -0,0 +1,52 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the AGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
Item
{
id: upgradeSelectionMachineAction
anchors.fill: parent
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Check Printer")
wrapMode: Text.WordWrap
font.pointSize: 18;
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker Original");
}
CheckBox
{
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@label", "Self-built heated bed")
checked: manager.hasHeatedBed
onClicked: manager.hasHeatedBed ? manager.removeHeatedBed() : manager.addHeatedBed()
}
UM.I18nCatalog { id: catalog; name: "cura"; }
}
}

View file

@ -1,6 +1,9 @@
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UpgradeFirmwareMachineAction(MachineAction):
def __init__(self):
super().__init__("UpgradeFirmware", "Upgrade Firmware")
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Upgrade Firmware"))
self._qml_url = "UpgradeFirmwareMachineAction.qml"

View file

@ -4,6 +4,7 @@
from . import BedLevelMachineAction
from . import UpgradeFirmwareMachineAction
from . import UMOCheckupMachineAction
from . import UMOUpgradeSelection
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
@ -20,4 +21,4 @@ def getMetaData():
}
def register(app):
return { "machine_action": [BedLevelMachineAction.BedLevelMachineAction(), UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(), UMOCheckupMachineAction.UMOCheckupMachineAction()]}
return { "machine_action": [BedLevelMachineAction.BedLevelMachineAction(), UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(), UMOCheckupMachineAction.UMOCheckupMachineAction(), UMOUpgradeSelection.UMOUpgradeSelection()]}

View file

@ -0,0 +1,100 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import UM.VersionUpgrade #To indicate that a file is of incorrect format.
import configparser #To read config files.
import io #To write config files to strings as if they were files.
## Creates a new machine instance instance by parsing a serialised machine
# instance in version 1 of the file format.
#
# \param serialised The serialised form of a machine instance in version 1.
# \param filename The supposed file name of this machine instance, without
# extension.
# \return A machine instance instance, or None if the file format is
# incorrect.
def importFrom(serialised, filename):
try:
return MachineInstance(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
return None
## A representation of a machine instance used as intermediary form for
# conversion from one format to the other.
class MachineInstance:
## Reads version 1 of the file format, storing it in memory.
#
# \param serialised A string with the contents of a machine instance file,
# without extension.
# \param filename The supposed file name of this machine instance.
def __init__(self, serialised, filename):
self._filename = filename
config = configparser.ConfigParser(interpolation = None)
config.read_string(serialised) # Read the input string as config file.
# Checking file correctness.
if not config.has_section("general"):
raise UM.VersionUpgrade.FormatException("No \"general\" section.")
if not config.has_option("general", "version"):
raise UM.VersionUpgrade.FormatException("No \"version\" in \"general\" section.")
if not config.has_option("general", "name"):
raise UM.VersionUpgrade.FormatException("No \"name\" in \"general\" section.")
if not config.has_option("general", "type"):
raise UM.VersionUpgrade.FormatException("No \"type\" in \"general\" section.")
if int(config.get("general", "version")) != 1: # Explicitly hard-code version 1, since if this number changes the programmer MUST change this entire function.
raise UM.VersionUpgrade.InvalidVersionException("The version of this machine instance is wrong. It must be 1.")
self._type_name = config.get("general", "type")
self._variant_name = config.get("general", "variant", fallback = "empty")
self._name = config.get("general", "name", fallback = "")
self._key = config.get("general", "key", fallback = None)
self._active_profile_name = config.get("general", "active_profile", fallback = "empty")
self._active_material_name = config.get("general", "material", fallback = "empty")
self._machine_setting_overrides = {}
for key, value in config["machine_settings"].items():
self._machine_setting_overrides[key] = value
## Serialises this machine instance as file format version 2.
#
# This is where the actual translation happens in this case.
#
# \return A tuple containing the new filename and a serialised form of
# this machine instance, serialised in version 2 of the file format.
def export(self):
config = configparser.ConfigParser(interpolation = None) # Build a config file in the form of version 2.
config.add_section("general")
config.set("general", "name", self._name)
config.set("general", "id", self._name)
config.set("general", "type", self._type_name)
config.set("general", "version", "2") # Hard-code version 2, since if this number changes the programmer MUST change this entire function.
import VersionUpgrade21to22 # Import here to prevent circular dependencies.
type_name = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translatePrinter(self._type_name)
active_profile = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateProfile(self._active_profile_name)
active_material = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateProfile(self._active_material_name)
variant = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariant(self._variant_name, type_name)
containers = [
self._name + "_current_settings",
active_profile,
active_material,
variant,
type_name
]
config.set("general", "containers", ",".join(containers))
config.add_section("metadata")
config.set("metadata", "type", "machine")
VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._machine_setting_overrides)
config.add_section("values")
for key, value in self._machine_setting_overrides.items():
config.set("values", key, str(value))
output = io.StringIO()
config.write(output)
return self._filename, output.getvalue()

View file

@ -0,0 +1,80 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #To read config files.
import io #To output config files to string.
import UM.VersionUpgrade #To indicate that a file is of the wrong format.
## Creates a new preferences instance by parsing a serialised preferences file
# in version 1 of the file format.
#
# \param serialised The serialised form of a preferences file in version 1.
# \param filename The supposed filename of the preferences file, without
# extension.
# \return A representation of those preferences, or None if the file format is
# incorrect.
def importFrom(serialised, filename):
try:
return Preferences(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
return None
## A representation of preferences files as intermediary form for conversion
# from one format to the other.
class Preferences:
## Reads version 2 of the preferences file format, storing it in memory.
#
# \param serialised A serialised version 2 preferences file.
# \param filename The supposed filename of the preferences file, without
# extension.
def __init__(self, serialised, filename):
self._filename = filename
self._config = configparser.ConfigParser(interpolation = None)
self._config.read_string(serialised)
#Checking file correctness.
if not self._config.has_section("general"):
raise UM.VersionUpgrade.FormatException("No \"general\" section.")
if not self._config.has_option("general", "version"):
raise UM.VersionUpgrade.FormatException("No \"version\" in \"general\" section.")
if int(self._config.get("general", "version")) != 2: # Explicitly hard-code version 2, since if this number changes the programmer MUST change this entire function.
raise UM.VersionUpgrade.InvalidVersionException("The version of this preferences file is wrong. It must be 2.")
if self._config.has_option("general", "name"): #This is probably a machine instance.
raise UM.VersionUpgrade.FormatException("There is a \"name\" field in this configuration file. I suspect it is not a preferences file.")
## Serialises these preferences as a preferences file of version 3.
#
# This is where the actual translation happens.
#
# \return A tuple containing the new filename and a serialised version of
# a preferences file in version 3.
def export(self):
#Reset the cura/categories_expanded property since it works differently now.
if self._config.has_section("cura") and self._config.has_option("cura", "categories_expanded"):
self._config.remove_option("cura", "categories_expanded")
#Translate the setting names in the visible settings.
if self._config.has_section("machines") and self._config.has_option("machines", "setting_visibility"):
visible_settings = self._config.get("machines", "setting_visibility")
visible_settings = visible_settings.split(",")
import VersionUpgrade21to22 #Import here to prevent a circular dependency.
visible_settings = [VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettingName(setting_name)
for setting_name in visible_settings]
visible_settings = ",".join(visible_settings)
self._config.set("machines", "setting_visibility", value = visible_settings)
#Translate the active_instance key.
if self._config.has_section("machines") and self._config.has_option("machines", "active_instance"):
active_machine = self._config.get("machines", "active_instance")
self._config.remove_option("machines", "active_instance")
self._config.set("cura", "active_machine", active_machine)
#Update the version number itself.
self._config.set("general", "version", value = "3")
#Output the result as a string.
output = io.StringIO()
self._config.write(output)
return self._filename, output.getvalue()

View file

@ -0,0 +1,133 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #To read config files.
import io #To write config files to strings as if they were files.
import UM.VersionUpgrade
## Creates a new profile instance by parsing a serialised profile in version 1
# of the file format.
#
# \param serialised The serialised form of a profile in version 1.
# \param filename The supposed filename of the profile, without extension.
# \return A profile instance, or None if the file format is incorrect.
def importFrom(serialised, filename):
try:
return Profile(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
return None
## A representation of a profile used as intermediary form for conversion from
# one format to the other.
class Profile:
## Reads version 1 of the file format, storing it in memory.
#
# \param serialised A string with the contents of a profile.
# \param filename The supposed filename of the profile, without extension.
def __init__(self, serialised, filename):
self._filename = filename
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
# Check correctness.
if not parser.has_section("general"):
raise UM.VersionUpgrade.FormatException("No \"general\" section.")
if not parser.has_option("general", "version"):
raise UM.VersionUpgrade.FormatException("No \"version\" in the \"general\" section.")
if int(parser.get("general", "version")) != 1: # Hard-coded profile version here. If this number changes the entire function needs to change.
raise UM.VersionUpgrade.InvalidVersionException("The version of this profile is wrong. It must be 1.")
# Parse the general section.
self._name = parser.get("general", "name")
self._type = parser.get("general", "type", fallback = None)
if "weight" in parser["general"]:
self._weight = int(parser.get("general", "weight"))
else:
self._weight = None
self._machine_type_id = parser.get("general", "machine_type", fallback = None)
self._machine_variant_name = parser.get("general", "machine_variant", fallback = None)
self._machine_instance_name = parser.get("general", "machine_instance", fallback = None)
if "material" in parser["general"]:
self._material_name = parser.get("general", "material")
elif self._type == "material":
self._material_name = parser.get("general", "name", fallback = None)
else:
self._material_name = None
# Parse the settings.
self._settings = {}
if parser.has_section("settings"):
for key, value in parser["settings"].items():
self._settings[key] = value
# Parse the defaults and the disabled defaults.
self._changed_settings_defaults = {}
if parser.has_section("defaults"):
for key, value in parser["defaults"].items():
self._changed_settings_defaults[key] = value
self._disabled_settings_defaults = []
if parser.has_section("disabled_defaults"):
disabled_defaults_string = parser.get("disabled_defaults", "values")
self._disabled_settings_defaults = [item for item in disabled_defaults_string.split(",") if item != ""] # Split by comma.
## Serialises this profile as file format version 2.
#
# \return A tuple containing the new filename and a serialised form of
# this profile, serialised in version 2 of the file format.
def export(self):
import VersionUpgrade21to22 # Import here to prevent circular dependencies.
if self._name == "Current settings":
self._filename += "_current_settings" #This resolves a duplicate ID arising from how Cura 2.1 stores its current settings.
config = configparser.ConfigParser(interpolation = None)
config.add_section("general")
config.set("general", "version", "2") #Hard-coded profile version 2.
config.set("general", "name", self._name)
if self._machine_type_id:
translated_machine = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translatePrinter(self._machine_type_id)
config.set("general", "definition", translated_machine)
else:
config.set("general", "definition", "fdmprinter")
config.add_section("metadata")
if self._type:
config.set("metadata", "type", self._type)
else:
config.set("metadata", "type", "quality")
if self._weight:
config.set("metadata", "weight", self._weight)
if self._machine_variant_name:
if self._machine_type_id:
config.set("metadata", "variant", VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariant(self._machine_variant_name, self._machine_type_id))
else:
config.set("metadata", "variant", self._machine_variant_name)
if self._material_name and self._type != "material":
config.set("metadata", "material", self._material_name)
if self._settings:
VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._settings)
config.add_section("values")
for key, value in self._settings.items():
config.set("values", key, str(value))
if self._changed_settings_defaults:
VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._changed_settings_defaults)
config.add_section("defaults")
for key, value in self._changed_settings_defaults.items():
config.set("defaults", key, str(value))
if self._disabled_settings_defaults:
disabled_settings_defaults = [VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettingName(setting)
for setting in self._disabled_settings_defaults]
config.add_section("disabled_defaults")
disabled_defaults_string = str(disabled_settings_defaults[0]) #Must be at least 1 item, otherwise we wouldn't enter this if statement.
for item in disabled_settings_defaults[1:]:
disabled_defaults_string += "," + str(item)
output = io.StringIO()
config.write(output)
return self._filename, output.getvalue()

View file

@ -0,0 +1,181 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #To get version numbers from config files.
from UM.VersionUpgrade import VersionUpgrade # Superclass of the plugin.
from . import MachineInstance # To upgrade machine instances.
from . import Preferences #To upgrade preferences.
from . import Profile # To upgrade profiles.
## How to translate printer names from the old version to the new.
_printer_translations = {
"ultimaker2plus": "ultimaker2_plus"
}
## How to translate profile names from the old version to the new.
_profile_translations = {
"PLA": "generic_pla",
"ABS": "generic_abs",
"CPE": "generic_cpe"
}
## How to translate setting names from the old version to the new.
_setting_name_translations = {
"remove_overlapping_walls_0_enabled": "travel_compensate_overlapping_walls_0_enabled",
"remove_overlapping_walls_enabled": "travel_compensate_overlapping_walls_enabled",
"remove_overlapping_walls_x_enabled": "travel_compensate_overlapping_walls_x_enabled",
"retraction_hop": "retraction_hop_enabled",
"speed_support_lines": "speed_support_infill"
}
## How to translate variants of specific machines from the old version to the
# new.
_variant_translations = {
"ultimaker2_plus": {
"0.25 mm": "ultimaker2_plus_0.25",
"0.4 mm": "ultimaker2_plus_0.4",
"0.6 mm": "ultimaker2_plus_0.6",
"0.8 mm": "ultimaker2_plus_0.8"
},
"ultimaker2_extended_plus": {
"0.25 mm": "ultimaker2_extended_plus_0.25",
"0.4 mm": "ultimaker2_extended_plus_0.4",
"0.6 mm": "ultimaker2_extended_plus_0.6",
"0.8 mm": "ultimaker2_extended_plus_0.8"
}
}
## Converts configuration from Cura 2.1's file formats to Cura 2.2's.
#
# It converts the machine instances and profiles.
class VersionUpgrade21to22(VersionUpgrade):
## Gets the version number from a config file.
#
# In all config files that concern this version upgrade, the version
# number is stored in general/version, so get the data from that key.
#
# \param serialised The contents of a config file.
# \return \type{int} The version number of that config file.
def getCfgVersion(self, serialised):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
return int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
## Converts machine instances from format version 1 to version 2.
#
# \param serialised The serialised machine instance in version 1.
# \param filename The supposed file name of the machine instance, without
# extension.
# \return A tuple containing the new filename and the serialised machine
# instance in version 2, or None if the input was not of the correct
# format.
def upgradeMachineInstance(self, serialised, filename):
machine_instance = MachineInstance.importFrom(serialised, filename)
if not machine_instance: #Invalid file format.
return filename, None
return machine_instance.export()
## Converts preferences from format version 2 to version 3.
#
# \param serialised The serialised preferences file in version 2.
# \param filename THe supposed file name of the preferences file, without
# extension.
# \return A tuple containing the new filename and the serialised
# preferences in version 3, or None if the input was not of the correct
# format.
def upgradePreferences(self, serialised, filename):
preferences = Preferences.importFrom(serialised, filename)
if not preferences: #Invalid file format.
return filename, None
return preferences.export()
## Converts profiles from format version 1 to version 2.
#
# \param serialised The serialised profile in version 1.
# \param filename The supposed file name of the profile, without
# extension.
# \return A tuple containing the new filename and the serialised profile
# in version 2, or None if the input was not of the correct format.
def upgradeProfile(self, serialised, filename):
profile = Profile.importFrom(serialised, filename)
if not profile: # Invalid file format.
return filename, None
return profile.export()
## Translates a printer name that might have changed since the last
# version.
#
# \param printer A printer name in Cura 2.1.
# \return The name of the corresponding printer in Cura 2.2.
@staticmethod
def translatePrinter(printer):
if printer in _printer_translations:
return _printer_translations[printer]
return printer #Doesn't need to be translated.
## Translates a built-in profile name that might have changed since the
# last version.
#
# \param profile A profile name in the old version.
# \return The corresponding profile name in the new version.
@staticmethod
def translateProfile(profile):
if profile in _profile_translations:
return _profile_translations[profile]
return profile #Doesn't need to be translated.
## Updates settings for the change from Cura 2.1 to 2.2.
#
# The keys and values of settings are changed to what they should be in
# the new version. Each setting is changed in-place in the provided
# dictionary. This changes the input parameter.
#
# \param settings A dictionary of settings (as key-value pairs) to update.
# \return The same dictionary.
@staticmethod
def translateSettings(settings):
for key, value in settings.items():
if key == "fill_perimeter_gaps": #Setting is removed.
del settings[key]
elif key == "remove_overlapping_walls_0_enabled": #Setting is functionally replaced.
del settings[key]
settings["travel_compensate_overlapping_walls_0_enabled"] = value
elif key == "remove_overlapping_walls_enabled": #Setting is functionally replaced.
del settings[key]
settings["travel_compensate_overlapping_walls_enabled"] = value
elif key == "remove_overlapping_walls_x_enabled": #Setting is functionally replaced.
del settings[key]
settings["travel_compensate_overlapping_walls_x_enabled"] = value
elif key == "retraction_combing": #Combing was made into an enum instead of a boolean.
settings[key] = "off" if (value == "False") else "all"
elif key == "retraction_hop": #Setting key was changed.
del settings[key]
settings["retraction_hop_enabled"] = value
elif key == "speed_support_lines": #Setting key was changed.
del settings[key]
settings["speed_support_infill"] = value
return settings
## Translates a setting name for the change from Cura 2.1 to 2.2.
#
# \param setting The name of a setting in Cura 2.1.
# \return The name of the corresponding setting in Cura 2.2.
@staticmethod
def translateSettingName(setting):
if setting in _setting_name_translations:
return _setting_name_translations[setting]
return setting #Doesn't need to be translated.
## Translates a variant name for the change from Cura 2.1 to 2.2
#
# \param variant The name of a variant in Cura 2.1.
# \param machine The name of the machine this variant is part of in Cura
# 2.2's naming.
# \return The name of the corresponding variant in Cura 2.2.
@staticmethod
def translateVariant(variant, machine):
if machine in _variant_translations and variant in _variant_translations[machine]:
return _variant_translations[machine][variant]
return variant

View file

@ -0,0 +1,43 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from . import VersionUpgrade21to22
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade21to22.VersionUpgrade21to22()
def getMetaData():
return {
"plugin": {
"name": catalog.i18nc("@label", "Version Upgrade 2.1 to 2.2"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Upgrades configurations from Cura 2.1 to Cura 2.2."),
"api": 3
},
"version_upgrade": {
# From To Upgrade function
("profile", 1): ("quality", 2, upgrade.upgradeProfile),
("machine_instance", 1): ("machine_stack", 2, upgrade.upgradeMachineInstance),
("preferences", 2): ("preferences", 3, upgrade.upgradePreferences)
},
"sources": {
"profile": {
"get_version": upgrade.getCfgVersion,
"location": {"./profiles", "./instance_profiles"}
},
"machine_instance": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
}
}
}
def register(app):
return { "version_upgrade": upgrade }

View file

@ -3,32 +3,230 @@
import math
import copy
import io
import xml.etree.ElementTree as ET
import uuid
from UM.Logger import Logger
import UM.Dictionary
import UM.Settings
# The namespace is prepended to the tag name but between {}.
# We are only interested in the actual tag name, so discard everything
# before the last }
def _tag_without_namespace(element):
return element.tag[element.tag.rfind("}") + 1:]
## Handles serializing and deserializing material containers from an XML file
class XmlMaterialProfile(UM.Settings.InstanceContainer):
def __init__(self, container_id, *args, **kwargs):
super().__init__(container_id, *args, **kwargs)
def serialize(self):
raise NotImplementedError("Writing material profiles has not yet been implemented")
## Overridden from InstanceContainer
def duplicate(self, new_id, new_name = None):
base_file = self.getMetaDataEntry("base_file", None)
new_uuid = str(uuid.uuid4())
if base_file:
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = base_file)
if containers:
new_basefile = containers[0].duplicate(self.getMetaDataEntry("brand") + "_" + new_id, new_name)
new_basefile.setMetaDataEntry("GUID", new_uuid)
base_file = new_basefile.id
UM.Settings.ContainerRegistry.getInstance().addContainer(new_basefile)
new_id = self.getMetaDataEntry("brand") + "_" + new_id + "_" + self.getDefinition().getId()
variant = self.getMetaDataEntry("variant")
if variant:
variant_containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = variant)
if variant_containers:
new_id += "_" + variant_containers[0].getName().replace(" ", "_")
result = super().duplicate(new_id, new_name)
result.setMetaDataEntry("GUID", new_uuid)
if result.getMetaDataEntry("base_file", None):
result.setMetaDataEntry("base_file", base_file)
return result
## Overridden from InstanceContainer
def setReadOnly(self, read_only):
super().setReadOnly(read_only)
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(GUID = self.getMetaDataEntry("GUID")):
container._read_only = read_only
## Overridden from InstanceContainer
def setMetaDataEntry(self, key, value):
if self.isReadOnly():
return
super().setMetaDataEntry(key, value)
if key == "material":
self.setName(value)
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(GUID = self.getMetaDataEntry("GUID")):
container.setMetaData(copy.deepcopy(self._metadata))
if key == "material":
container.setName(value)
## Overridden from InstanceContainer
def setProperty(self, key, property_name, property_value, container = None):
if self.isReadOnly():
return
super().setProperty(key, property_name, property_value)
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(GUID = self.getMetaDataEntry("GUID")):
container._dirty = True
## Overridden from InstanceContainer
def serialize(self):
registry = UM.Settings.ContainerRegistry.getInstance()
base_file = self.getMetaDataEntry("base_file", "")
if base_file and self.id != base_file:
# Since we create an instance of XmlMaterialProfile for each machine and nozzle in the profile,
# we should only serialize the "base" material definition, since that can then take care of
# serializing the machine/nozzle specific profiles.
raise NotImplementedError("Cannot serialize non-root XML materials")
builder = ET.TreeBuilder()
root = builder.start("fdmmaterial", { "xmlns": "http://www.ultimaker.com/material"})
## Begin Metadata Block
builder.start("metadata")
metadata = copy.deepcopy(self.getMetaData())
properties = metadata.pop("properties", {})
# Metadata properties that should not be serialized.
metadata.pop("status", "")
metadata.pop("variant", "")
metadata.pop("type", "")
metadata.pop("base_file", "")
## Begin Name Block
builder.start("name")
builder.start("brand")
builder.data(metadata.pop("brand", ""))
builder.end("brand")
builder.start("material")
builder.data(metadata.pop("material", ""))
builder.end("material")
builder.start("color")
builder.data(metadata.pop("color_name", ""))
builder.end("color")
builder.end("name")
## End Name Block
for key, value in metadata.items():
builder.start(key)
builder.data(value)
builder.end(key)
builder.end("metadata")
## End Metadata Block
## Begin Properties Block
builder.start("properties")
for key, value in properties.items():
builder.start(key)
builder.data(value)
builder.end(key)
builder.end("properties")
## End Properties Block
## Begin Settings Block
builder.start("settings")
if self.getDefinition().id == "fdmprinter":
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {}
machine_nozzle_map = {}
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"))
for container in all_containers:
definition_id = container.getDefinition().id
if definition_id == "fdmprinter":
continue
if definition_id not in machine_container_map:
machine_container_map[definition_id] = container
if definition_id not in machine_nozzle_map:
machine_nozzle_map[definition_id] = {}
variant = container.getMetaDataEntry("variant")
if variant:
machine_nozzle_map[definition_id][variant] = container
continue
machine_container_map[definition_id] = container
for definition_id, container in machine_container_map.items():
definition = container.getDefinition()
try:
product = UM.Dictionary.findKey(self.__product_id_map, definition_id)
except ValueError:
continue
builder.start("machine")
builder.start("machine_identifier", { "manufacturer": definition.getMetaDataEntry("manufacturer", ""), "product": product})
builder.end("machine_identifier")
for instance in container.findInstances():
if self.getDefinition().id == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the base profile, just skip since we inherit the base profile.
continue
self._addSettingElement(builder, instance)
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
for hotend_id, hotend in machine_nozzle_map[definition_id].items():
variant_containers = registry.findInstanceContainers(id = hotend.getMetaDataEntry("variant"))
if not variant_containers:
continue
builder.start("hotend", { "id": variant_containers[0].getName() })
for instance in hotend.findInstances():
if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the machine profile, just skip since we inherit the machine profile.
continue
self._addSettingElement(builder, instance)
builder.end("hotend")
builder.end("machine")
builder.end("settings")
## End Settings Block
builder.end("fdmmaterial")
root = builder.close()
_indent(root)
stream = io.StringIO()
tree = ET.ElementTree(root)
tree.write(stream, "unicode", True)
return stream.getvalue()
## Overridden from InstanceContainer
def deserialize(self, serialized):
data = ET.fromstring(serialized)
self.addMetaDataEntry("type", "material")
# TODO: Add material verfication
self.addMetaDataEntry("status", "Unknown")
self.addMetaDataEntry("status", "unknown")
metadata = data.iterfind("./um:metadata/*", self.__namespaces)
for entry in metadata:
@ -39,7 +237,7 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
material = entry.find("./um:material", self.__namespaces)
color = entry.find("./um:color", self.__namespaces)
self.setName("{0} {1} ({2})".format(brand.text, material.text, color.text))
self.setName(material.text)
self.addMetaDataEntry("brand", brand.text)
self.addMetaDataEntry("material", material.text)
@ -49,6 +247,12 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
self.addMetaDataEntry(tag_name, entry.text)
if not "description" in self.getMetaData():
self.addMetaDataEntry("description", "")
if not "adhesion_info" in self.getMetaData():
self.addMetaDataEntry("adhesion_info", "")
property_values = {}
properties = data.iterfind("./um:properties/*", self.__namespaces)
for entry in properties:
@ -58,17 +262,6 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
diameter = float(property_values.get("diameter", 2.85)) # In mm
density = float(property_values.get("density", 1.3)) # In g/cm3
weight_per_cm = (math.pi * (diameter / 20) ** 2 * 0.1) * density
spool_weight = property_values.get("spool_weight")
spool_length = property_values.get("spool_length")
if spool_weight:
length = float(spool_weight) / weight_per_cm
property_values["spool_length"] = str(length / 100)
elif spool_length:
weight = (float(spool_length) * 100) * weight_per_cm
property_values["spool_weight"] = str(weight)
self.addMetaDataEntry("properties", property_values)
self.setDefinition(UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0])
@ -83,6 +276,8 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
else:
Logger.log("d", "Unsupported material setting %s", key)
self._dirty = False
machines = data.iterfind("./um:settings/um:machine", self.__namespaces)
for machine in machines:
machine_setting_values = {}
@ -112,6 +307,7 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
new_material.setName(self.getName())
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.setDefinition(definition)
new_material.addMetaDataEntry("base_file", self.id)
for key, value in global_setting_values.items():
new_material.setProperty(key, "value", value, definition)
@ -142,6 +338,7 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
new_hotend_material.setName(self.getName())
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.setDefinition(definition)
new_hotend_material.addMetaDataEntry("base_file", self.id)
new_hotend_material.addMetaDataEntry("variant", variant_containers[0].id)
@ -162,6 +359,15 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
new_hotend_material._dirty = False
UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material)
def _addSettingElement(self, builder, instance):
try:
key = UM.Dictionary.findKey(self.__material_property_setting_map, instance.definition.key)
except ValueError:
return
builder.start("setting", { "key": key })
builder.data(str(instance.value))
builder.end("setting")
# Map XML file setting names to internal names
__material_property_setting_map = {
@ -174,6 +380,7 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
}
# Map XML file product names to internal ids
# TODO: Move this to definition's metadata
__product_id_map = {
"Ultimaker2": "ultimaker2",
"Ultimaker2+": "ultimaker2_plus",
@ -184,6 +391,30 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer):
"Ultimaker Original+": "ultimaker_original_plus"
}
# Map of recognised namespaces with a proper prefix.
__namespaces = {
"um": "http://www.ultimaker.com/material"
}
## Helper function for pretty-printing XML because ETree is stupid
def _indent(elem, level = 0):
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
_indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
# The namespace is prepended to the tag name but between {}.
# We are only interested in the actual tag name, so discard everything
# before the last }
def _tag_without_namespace(element):
return element.tag[element.tag.rfind("}") + 1:]

View file

@ -17,6 +17,7 @@ def getMetaData():
"api": 3
},
"settings_container": {
"type": "material",
"mimetype": "application/x-ultimaker-material-profile"
}
}