Merge branch 'master' into themed-layer-view

This commit is contained in:
fieldOfView 2017-04-25 18:06:12 +02:00
commit 2493bd6fe6
289 changed files with 90651 additions and 67915 deletions

View file

@ -47,7 +47,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
return self._id_mapping[old_id]
def preRead(self, file_name):
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
pass
@ -167,6 +167,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "File %s is not a valid workspace.", file_name)
return WorkspaceReader.PreReadResult.failed
# In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
if not show_dialog:
return WorkspaceReader.PreReadResult.accepted
# Show the dialog, informing the user what is about to happen.
self._dialog.setMachineConflict(machine_conflict)
self._dialog.setQualityChangesConflict(quality_changes_conflict)

View file

@ -12,15 +12,15 @@ UM.Dialog
{
title: catalog.i18nc("@title:window", "Open Project")
width: 550
minimumWidth: 550
maximumWidth: 550
width: 550 * Screen.devicePixelRatio
minimumWidth: 550 * Screen.devicePixelRatio
maximumWidth: minimumWidth
height: 400
minimumHeight: 400
maximumHeight: 400
property int comboboxHeight: 15
property int spacerHeight: 10
height: 400 * Screen.devicePixelRatio
minimumHeight: 400 * Screen.devicePixelRatio
maximumHeight: minimumHeight
property int comboboxHeight: 15 * Screen.devicePixelRatio
property int spacerHeight: 10 * Screen.devicePixelRatio
onClosing: manager.notifyClosed()
onVisibleChanged:
{
@ -33,20 +33,17 @@ UM.Dialog
}
Item
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
anchors.bottomMargin: 20
anchors.leftMargin:20
anchors.rightMargin: 20
anchors.fill: parent
anchors.margins: 20 * Screen.devicePixelRatio
UM.I18nCatalog
{
id: catalog;
name: "cura";
id: catalog
name: "cura"
}
SystemPalette
{
id: palette
}
ListModel
@ -70,12 +67,12 @@ UM.Dialog
{
id: titleLabel
text: catalog.i18nc("@action:title", "Summary - Cura Project")
font.pixelSize: 22
font.pointSize: 18
}
Rectangle
{
id: separator
color: "black"
color: palette.text
width: parent.width
height: 1
}
@ -93,7 +90,7 @@ UM.Dialog
{
text: catalog.i18nc("@action:label", "Printer settings")
font.bold: true
width: parent.width /3
width: parent.width / 3
}
Item
{
@ -360,7 +357,7 @@ UM.Dialog
height: width
source: UM.Theme.getIcon("notice")
color: "black"
color: palette.text
}
Label
@ -392,4 +389,4 @@ UM.Dialog
anchors.right: parent.right
}
}
}
}

View file

@ -1,9 +1,16 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
from typing import Dict
import sys
from UM.Logger import Logger
try:
from . import ThreeMFReader
except ImportError:
Logger.log("w", "Could not import ThreeMFReader; libSavitar may be missing")
from . import ThreeMFReader
from . import ThreeMFWorkspaceReader
from UM.i18n import i18nCatalog
from UM.Platform import Platform
catalog = i18nCatalog("cura")
@ -14,30 +21,36 @@ def getMetaData() -> Dict:
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
return {
metaData = {
"plugin": {
"name": catalog.i18nc("@label", "3MF Reader"),
"author": "Ultimaker",
"version": "1.0",
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading 3MF files."),
"api": 3
},
"mesh_reader": [
}
}
if "3MFReader.ThreeMFReader" in sys.modules:
metaData["mesh_reader"] = [
{
"extension": "3mf",
"description": catalog.i18nc("@item:inlistbox", "3MF File")
}
],
"workspace_reader":
[
]
metaData["workspace_reader"] = [
{
"extension": workspace_extension,
"description": catalog.i18nc("@item:inlistbox", "3MF File")
}
]
}
return metaData
def register(app):
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
"workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()}
if "3MFReader.ThreeMFReader" in sys.modules:
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
"workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()}
else:
return {}

View file

@ -1,30 +1,39 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
import sys
from UM.Logger import Logger
try:
from . import ThreeMFWriter
except ImportError:
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
from . import ThreeMFWorkspaceWriter
from UM.i18n import i18nCatalog
from . import ThreeMFWorkspaceWriter
from . import ThreeMFWriter
i18n_catalog = i18nCatalog("uranium")
def getMetaData():
return {
metaData = {
"plugin": {
"name": i18n_catalog.i18nc("@label", "3MF Writer"),
"author": "Ultimaker",
"version": "1.0",
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides support for writing 3MF files."),
"api": 3
},
"mesh_writer": {
}
}
if "3MFWriter.ThreeMFWriter" in sys.modules:
metaData["mesh_writer"] = {
"output": [{
"extension": "3mf",
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
}]
},
"workspace_writer": {
}
metaData["workspace_writer"] = {
"output": [{
"extension": "curaproject.3mf",
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
@ -32,7 +41,12 @@ def getMetaData():
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
}]
}
}
return metaData
def register(app):
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
if "3MFWriter.ThreeMFWriter" in sys.modules:
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
else:
return {}

View file

@ -1,56 +1,54 @@
[2.5.0]
*Speed.
Weve given the system a tweak, to make changing printers, profiles, materials, and print cores even quicker than ever. 3MF processing is also much faster now. Opening a 3MF file now takes one tenth of the time. That means less hanging around, more time printing.
*Improved speed
Weve made changing printers, profiles, materials, and print cores even faster. 3MF processing is also much faster now. Opening a 3MF file now takes one tenth of the time.
*Speedup engine Multi-threading.
This is one of the most significant improvements, making slicing even faster. Just like computers with multiple cores, Cura can process multiple operations at the same time. Hows that for efficient?
*Speedup engine Multithreading
Cura can process multiple operations at the same time during slicing. Supported by Windows and Linux operating systems only.
*Better layout for 3D layer view options.
Need things to be a bit clearer? Weve now incorporated an improved layer view for computers that support OpenGL 4.1. For OpenGL 2.0 we will automatically switch to the old layer view. Thanks to community member Aldo Hoeben for the fancy double handle slider.
*Preheat the build plate (with a connected printer)
Users can now set the Ultimaker 3 to preheat the build plate, which reduces the downtime, allowing to manually speed up the printing workflow.
*Disable automatic slicing.
Some users told us that slicing slowed down their workflow when it auto-starts, and to improve the user experience, we added an option to disable auto-slicing if required. Thanks to community member Aldo Hoeben for contributing to this one.
*Better layout for 3D layer view options
An improved layer view has been implemented for computers that support OpenGL 4.1. For OpenGL 2.0 to 4.0, we will automatically switch to the old layer view.
*Auto-scale off by default.
This change needs no explanation!
*Disable automatic slicing
An option to disable auto-slicing has been added for the better user experience.
*Preheat the build plate (with a connected printer).
You can now set your Ultimaker 3 to preheat the build plate, which reduces the downtime, letting you manually speed up your printing workflow. All you need to do is use the preheat function in the Print Monitor screen, and set the correct temperature for the active material(s).
*Auto-scale off by default
This change speaks for itself.
*G-code reader.
The g-code reader has been reintroduced, which means you can load g-code from file and display it in layer view. You can also print saved g-code files with Cura, share and re-use them, and you can check that your printed object looks right via the g-code viewer. Thanks to AlephObjects for this feature.
*Print cost calculation
The latest version of Cura now contains code to help users calculate the cost of their prints. To do so, users need to enter a cost per spool and an amount of materials per spool. It is also possible to set the cost per material and gain better control of the expenses. Thanks to our community member Aldo Hoeben for adding this feature.
*Switching profiles.
When you change a printing profile after customizing print settings, you have an option (shown in a popup) to transfer your customizations to the new profile or discard those modifications and continue with default settings instead. Weve made this dialog window more informative and intuitive.
*G-code reader
The g-code reader has been reintroduced, which means users can load g-code from file and display it in layer view. Users can also print saved g-code files with Cura, share and re-use them, as well as preview the printed object via the g-code viewer. Thanks to AlephObjects for this feature.
*Print cost calculation.
Cura now contains code to help you calculate the cost of your print. To do so, youll need to enter a cost per spool and an amount of materials per spool. You can also set the cost per material and gain better control of your expenses. Thanks to our community member Aldo Hoeben for adding this feature.
*Discard or Keep Changes popup
Weve changed the popup that appears when a user changes a printing profile after setting custom printing settings. It is now more informative and helpful.
*Bug fixes
Property renaming: Properties that start with get have been renamed to avoid confusion.
Window overflow: This is now fixed.
Multiple machine prefixes: Multiple machine prefixes are gone when loading and saving projects.
Removal of file extension: When you save a file or project (without changing the file type), no file extension is added to the name. Its only when you change to another file type that the extension is added.
Ultimaker 3 Extended connectivity: Selecting Ultimaker 3 Extended in Cura let you connect and print with Ultimaker 3, without any warning. This now has been fixed.
Different Y / Z colors: Y and Z colors in the tool menu are now different to the colors on the build plate.
No collision areas: No collision areas were generated for some models.
Perimeter gaps: Perimeter gaps are not filled often enough; weve now amended this.
File location after restart: The old version of Cura didnt remember the last opened file location after its been restarted. Now it does!
Project name: The project name changes after the project is opened.
Slicing when error value is given (print core 2): When a support is printed with extruder 2 (PVA), some support settings will trigger a slice when an error value is given. Weve now sorted this out.
Support Towers: Support Towers can now be disabled.
Support bottoms: When putting one object on top of another with some space in between, and selecting support with support bottom interface, no support bottom is printed. This has now been resolved.
Summary box size: Weve enlarged the summary box when saving your project.
Cubic subdivision infill: In the past, the cubic subdivision infill sometimes didnt produce the infill (WIN) this has now been addressed.
Spiralize outer contour and fill small gaps: When combining Fill Gaps Between Walls with Spiralize Outer Contour, the model gets a massive infill.
Experimental post-processing plugin: Since the TwaekAtZ post-processing plugin is not officially supported, we added the Experimental tag.
- Window overflow: On some configurations (OS and screen dependant), an overflow on the General (Preferences) panel and the credits list on the About window occurred. This is now fixed.
- “Center camera when the item is selected”: This is now set to off by default.
- Removal of file extension: When users save a file or project (without changing the file type), no file extension is added to the name. Its only when users change to another file type that the extension is added.
- Ultimaker 3 Extended connectivity. Selecting Ultimaker 3 Extended in Cura let you connect and print with Ultimaker 3, without any warning. This now has been fixed.
- Different Y / Z colors: Y and Z colors in the tool menu are now similar to the colors on the build plate.
- No collision areas: No collision areas used to be generated for some models when "keep models apart" was activated. This is now fixed.
- Perimeter gaps: Perimeter gaps are not filled often enough; weve now amended this.
- File location after restart: The old version of Cura didnt remember the last opened file location after its been restarted. Now it has been fixed.
- Project name: The project name changes after the project is opened. This now has been fixed.
- Slicing when error value is given (print core 2): When a support is printed with the Extruder 2 (PVA), some support settings will trigger a slice when an error value is given. Weve now sorted this out.
- Support Towers: Support Towers can now be disabled.
- Support bottoms: When putting one object on top of another with some space in between, and selecting support with support bottom interface, no support bottom is printed. This has now been resolved.
- Summary box size: Weve enlarged the summary box when saving the project.
- Cubic subdivision infill: In the past, the cubic subdivision infill sometimes didnt produce the infill (WIN) this has now been addressed.
- Spiralize outer contour and fill small gaps: When combining Fill Gaps Between Walls with Spiralize Outer Contour, the model gets a massive infill.
- Experimental post-processing plugin: Since the TweakAtZ post-processing plugin is not officially supported, we added the Experimental tag.
*3rd party printers (bug fixes)
Folgertech printer definition has been added
Hello BEE Prusa printer definition has been added
Material profiles for Cartesio printers have been updated
- Folgertech printer definition has been added.
- Hello BEE Prusa printer definition has been added.
- Velleman Vertex K8400 printer definitions have been added for both single-extrusion and dual-extrusion versions.
- Material profiles for Cartesio printers have been updated.
[2.4.0]
*Project saving & opening

193
plugins/GCodeReader/GCodeReader.py Normal file → Executable file
View file

@ -2,6 +2,8 @@
# Cura is released under the terms of the AGPLv3 or higher.
from UM.Application import Application
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
@ -9,6 +11,7 @@ from UM.Mesh.MeshReader import MeshReader
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
@ -17,6 +20,7 @@ from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
@ -32,14 +36,21 @@ class GCodeReader(MeshReader):
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
self._scene_node = None
self._position = namedtuple('Position', ['x', 'y', 'z', 'e'])
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self):
self._extruder = 0
self._extruder_number = 0
self._layer_type = LayerPolygon.Inset0Type
self._layer = 0
self._layer_number = 0
self._previous_z = 0
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
self._center_is_zero = False
@ -82,18 +93,21 @@ class GCodeReader(MeshReader):
def _getNullBoundingBox():
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, current_z, path):
def _createPolygon(self, layer_thickness, path, extruder_offsets):
countvalid = 0
for point in path:
if point[3] > 0:
countvalid += 1
if countvalid >= 2:
# we know what to do now, no need to count further
continue
if countvalid < 2:
return False
try:
self._layer_data_builder.addLayer(self._layer)
self._layer_data_builder.setLayerHeight(self._layer, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer, math.fabs(current_z - self._previous_z))
this_layer = self._layer_data_builder.getLayer(self._layer)
self._layer_data_builder.addLayer(self._layer_number)
self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
this_layer = self._layer_data_builder.getLayer(self._layer_number)
except ValueError:
return False
count = len(path)
@ -101,22 +115,19 @@ class GCodeReader(MeshReader):
line_widths = numpy.empty((count - 1, 1), numpy.float32)
line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
# TODO: need to calculate actual line width based on E values
line_widths[:, 0] = 0.4
# TODO: need to calculate actual line heights
line_thicknesses[:, 0] = 0.2
line_widths[:, 0] = 0.35 # Just a guess
line_thicknesses[:, 0] = layer_thickness
points = numpy.empty((count, 3), numpy.float32)
i = 0
for point in path:
points[i, 0] = point[0]
points[i, 1] = point[2]
points[i, 2] = -point[1]
points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
if i > 0:
line_types[i - 1] = point[3]
if point[3] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
line_widths[i - 1] = 0.2
line_widths[i - 1] = 0.1
i += 1
this_poly = LayerPolygon(self._extruder, line_types, points, line_widths, line_thicknesses)
this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses)
this_poly.buildCache()
this_layer.polygons.append(this_poly)
@ -126,30 +137,28 @@ class GCodeReader(MeshReader):
x, y, z, e = position
x = params.x if params.x is not None else x
y = params.y if params.y is not None else y
z_changed = False
if params.z is not None:
if z != params.z:
z_changed = True
self._previous_z = z
z = params.z
z = params.z if params.z is not None else position.z
if params.e is not None:
if params.e > e[self._extruder]:
if params.e > e[self._extruder_number]:
path.append([x, y, z, self._layer_type]) # extrusion
else:
path.append([x, y, z, LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder] = params.e
e[self._extruder_number] = params.e
# Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
# Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z + 0.05 # allow a tiny overlap
self._previous_z = z
else:
path.append([x, y, z, LayerPolygon.MoveCombingType])
if z_changed:
if not self._is_layers_in_file:
if len(path) > 1 and z > 0:
if self._createPolygon(z, path):
self._layer += 1
path.clear()
else:
path.clear()
return self._position(x, y, z, e)
# G0 and G1 should be handled exactly the same.
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position, params, path):
return self._position(
params.x if params.x is not None else position.x,
@ -157,24 +166,36 @@ class GCodeReader(MeshReader):
0,
position.e)
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position, params, path):
if params.e is not None:
position.e[self._extruder] = params.e
position.e[self._extruder_number] = params.e
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
params.z if params.z is not None else position.z,
position.e)
_gCode1 = _gCode0
def _processGCode(self, G, line, position, path):
func = getattr(self, "_gCode%s" % G, None)
x = self._getFloat(line, "X")
y = self._getFloat(line, "Y")
z = self._getFloat(line, "Z")
e = self._getFloat(line, "E")
line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None:
s = line.upper().split(" ")
x, y, z, e = None, None, None, None
for item in s[1:]:
if len(item) <= 1:
continue
if item.startswith(";"):
continue
if item[0] == "X":
x = float(item[1:])
if item[0] == "Y":
y = float(item[1:])
if item[0] == "Z":
z = float(item[1:])
if item[0] == "E":
e = float(item[1:])
if (x is not None and x < 0) or (y is not None and y < 0):
self._center_is_zero = True
params = self._position(x, y, z, e)
@ -182,40 +203,46 @@ class GCodeReader(MeshReader):
return position
def _processTCode(self, T, line, position, path):
self._extruder = T
if self._extruder + 1 > len(position.e):
position.e.extend([0] * (self._extruder - len(position.e) + 1))
if not self._is_layers_in_file:
if len(path) > 1 and position[2] > 0:
if self._createPolygon(position[2], path):
self._layer += 1
path.clear()
else:
path.clear()
self._extruder_number = T
if self._extruder_number + 1 > len(position.e):
position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
return position
_type_keyword = ";TYPE:"
_layer_keyword = ";LAYER:"
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self):
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
def read(self, file_name):
Logger.log("d", "Preparing to load %s" % file_name)
self._cancelled = False
scene_node = SceneNode()
scene_node.getBoundingBox = self._getNullBoundingBox # Manually set bounding box, because mesh doesn't have mesh data
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
glist = []
gcode_list = []
self._is_layers_in_file = False
Logger.log("d", "Opening file %s" % file_name)
self._extruder_offsets = self._extruderOffsets() # dict with index the extruder number. can be empty
last_z = 0
with open(file_name, "r") as file:
file_lines = 0
current_line = 0
for line in file:
file_lines += 1
glist.append(line)
gcode_list.append(line)
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
file.seek(0)
@ -228,7 +255,7 @@ class GCodeReader(MeshReader):
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing %s" % file_name)
Logger.log("d", "Parsing %s..." % file_name)
current_position = self._position(0, 0, 0, [0])
current_path = []
@ -238,10 +265,14 @@ class GCodeReader(MeshReader):
Logger.log("d", "Parsing %s cancelled" % file_name)
return None
current_line += 1
last_z = current_position.z
if current_line % file_step == 0:
self._message.setProgress(math.floor(current_line / file_lines * 100))
Job.yieldThread()
if len(line) == 0:
continue
if line.find(self._type_keyword) == 0:
type = line[len(self._type_keyword):].strip()
if type == "WALL-INNER":
@ -256,28 +287,48 @@ class GCodeReader(MeshReader):
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
try:
layer_number = int(line[len(self._layer_keyword):])
self._createPolygon(current_position[2], current_path)
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
self._layer = layer_number
self._layer_number = layer_number
except:
pass
if line[0] == ";":
# This line is a comment. Ignore it (except for the layer_keyword)
if line.startswith(";"):
continue
G = self._getInt(line, "G")
if G is not None:
current_position = self._processGCode(G, line, current_position, current_path)
T = self._getInt(line, "T")
if T is not None:
current_position = self._processTCode(T, line, current_position, current_path)
if not self._is_layers_in_file and len(current_path) > 1 and current_position[2] > 0:
if self._createPolygon(current_position[2], current_path):
self._layer += 1
current_path.clear()
# < 2 is a heuristic for a movement only, that should not be counted as a layer
if current_position.z > last_z and abs(current_position.z - last_z) < 2:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
current_path.clear()
if not self._is_layers_in_file:
self._layer_number += 1
continue
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
current_position = self._processTCode(T, line, current_position, current_path)
# "Flush" leftovers
if not self._is_layers_in_file and len(current_path) > 1:
if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
@ -288,13 +339,15 @@ class GCodeReader(MeshReader):
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGCodeList(glist)
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()
if self._layer == 0:
if self._layer_number == 0:
Logger.log("w", "File %s doesn't contain any valid layers" % file_name)
settings = Application.getInstance().getGlobalContainerStack()
@ -306,4 +359,14 @@ class GCodeReader(MeshReader):
Logger.log("d", "Loaded %s" % file_name)
if Preferences.getInstance().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."), lifetime=0)
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node

View file

@ -21,7 +21,7 @@ class ImageReader(MeshReader):
self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"]
self._ui = ImageReaderUI(self)
def preRead(self, file_name):
def preRead(self, file_name, *args, **kwargs):
img = QImage(file_name)
if img.isNull():

View file

@ -1,6 +1,8 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import sys
from UM.PluginRegistry import PluginRegistry
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
@ -253,8 +255,17 @@ class LayerView(View):
if not layer_data:
continue
if new_max_layers < len(layer_data.getLayers()):
new_max_layers = len(layer_data.getLayers()) - 1
min_layer_number = sys.maxsize
max_layer_number = -sys.maxsize
for layer_id in layer_data.getLayers():
if max_layer_number < layer_id:
max_layer_number = layer_id
if min_layer_number > layer_id:
min_layer_number = layer_id
layer_count = max_layer_number - min_layer_number
if new_max_layers < layer_count:
new_max_layers = layer_count
if new_max_layers > 0 and new_max_layers != self._old_max_layers:
self._max_layers = new_max_layers

View file

@ -363,7 +363,7 @@ Item
property bool roundValues: true
property var activeHandle: upperHandle
property bool layersVisible: UM.LayerView.layerActivity && Printer.platformActivity ? true : false
property bool layersVisible: UM.LayerView.layerActivity && CuraApplication.platformActivity ? true : false
function getUpperValueFromHandle()
{

View file

@ -37,7 +37,8 @@ class RemovableDriveOutputDevice(OutputDevice):
# meshes.
# \param limit_mimetypes Should we limit the available MIME types to the
# MIME types available to the currently active machine?
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
#
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
if self._writing:
raise OutputDeviceError.DeviceBusyError()
@ -90,7 +91,7 @@ class RemovableDriveOutputDevice(OutputDevice):
self.writeStarted.emit(self)
job._message = message
job.setMessage(message)
self._writing = True
job.start()
except PermissionError as e:
@ -117,8 +118,6 @@ class RemovableDriveOutputDevice(OutputDevice):
raise OutputDeviceError.WriteRequestFailedError("Could not find a file name when trying to write to {device}.".format(device = self.getName()))
def _onProgress(self, job, progress):
if hasattr(job, "_message"):
job._message.setProgress(progress)
self.writeProgress.emit(self, progress)
def _onFinished(self, job):
@ -127,10 +126,6 @@ class RemovableDriveOutputDevice(OutputDevice):
self._stream.close()
self._stream = None
if hasattr(job, "_message"):
job._message.hide()
job._message = None
self._writing = False
self.writeFinished.emit(self)
if job.getResult():

View file

@ -35,6 +35,7 @@ class DiscoverUM3Action(MachineAction):
@pyqtSlot()
def startDiscovery(self):
if not self._network_plugin:
Logger.log("d", "Starting printer discovery.")
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
self.printersChanged.emit()
@ -42,6 +43,7 @@ class DiscoverUM3Action(MachineAction):
## Re-filters the list of printers.
@pyqtSlot()
def reset(self):
Logger.log("d", "Reset the list of found printers.")
self.printersChanged.emit()
@pyqtSlot()
@ -95,12 +97,14 @@ class DiscoverUM3Action(MachineAction):
@pyqtSlot(str)
def setKey(self, key):
Logger.log("d", "Attempting to set the network key of the active machine to %s", key)
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data:
global_container_stack.setMetaDataEntry("um_network_key", key)
# Delete old authentication data.
Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key)
global_container_stack.removeMetaDataEntry("network_authentication_id")
global_container_stack.removeMetaDataEntry("network_authentication_key")
else:

213
plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py Normal file → Executable file
View file

@ -200,11 +200,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
def _onAuthenticationRequired(self, reply, authenticator):
if self._authentication_id is not None and self._authentication_key is not None:
Logger.log("d", "Authentication was required. Setting up authenticator with ID %s",self._authentication_id )
Logger.log("d", "Authentication was required. Setting up authenticator with ID %s and key %s", self._authentication_id, self._getSafeAuthKey())
authenticator.setUser(self._authentication_id)
authenticator.setPassword(self._authentication_key)
else:
Logger.log("d", "No authentication was required. The ID is: %s", self._authentication_id)
Logger.log("d", "No authentication is available to use, but we did got a request for it.")
def getProperties(self):
return self._properties
@ -283,10 +283,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
#
# /param temperature The new target temperature of the bed.
def _setTargetBedTemperature(self, temperature):
if self._target_bed_temperature == temperature:
if not self._updateTargetBedTemperature(temperature):
return
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
url = QUrl("http://" + self._address + self._api_prefix + "printer/bed/temperature/target")
data = str(temperature)
@ -294,6 +292,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
self._manager.put(put_request, data.encode())
## Updates the target bed temperature from the printer, and emit a signal if it was changed.
#
# /param temperature The new target temperature of the bed.
# /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
def _updateTargetBedTemperature(self, temperature):
if self._target_bed_temperature == temperature:
return False
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
return True
def _stopCamera(self):
self._camera_timer.stop()
if self._image_reply:
@ -528,7 +537,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
bed_temperature = self._json_printer_state["bed"]["temperature"]["current"]
self._setBedTemperature(bed_temperature)
target_bed_temperature = self._json_printer_state["bed"]["temperature"]["target"]
self._setTargetBedTemperature(target_bed_temperature)
self._updateTargetBedTemperature(target_bed_temperature)
head_x = self._json_printer_state["heads"][0]["position"]["x"]
head_y = self._json_printer_state["heads"][0]["position"]["y"]
@ -601,7 +610,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
# This is ignored.
# \param filter_by_machine Whether to filter MIME types by machine. This
# is ignored.
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
# \param kwargs Keyword arguments.
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
if self._printer_state != "idle":
self._error_message = Message(
i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._printer_state)
@ -618,64 +628,67 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
print_information = Application.getInstance().getPrintInformation()
# Check if print cores / materials are loaded at all. Any failure in these results in an Error.
for index in range(0, self._num_extruders):
if print_information.materialLengths[index] != 0:
if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "":
Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1)
self._error_message = Message(
i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No PrinterCore loaded in slot {0}".format(index + 1)))
self._error_message.show()
return
if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "":
Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1)
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to start a new print job. No material loaded in slot {0}".format(index + 1)))
self._error_message.show()
return
warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about.
for index in range(0, self._num_extruders):
# Check if there is enough material. Any failure in these results in a warning.
material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"]
if material_length != -1 and print_information.materialLengths[index] > material_length:
Logger.log("w", "Printer reports that there is not enough material left for extruder %s. We need %s and the printer has %s", index + 1, print_information.materialLengths[index], material_length)
warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1))
# Only check for mistakes if there is material length information.
if print_information.materialLengths:
# Check if print cores / materials are loaded at all. Any failure in these results in an Error.
for index in range(0, self._num_extruders):
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "":
Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1)
self._error_message = Message(
i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No PrinterCore loaded in slot {0}".format(index + 1)))
self._error_message.show()
return
if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "":
Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1)
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to start a new print job. No material loaded in slot {0}".format(index + 1)))
self._error_message.show()
return
# Check if the right cartridges are loaded. Any failure in these results in a warning.
extruder_manager = cura.Settings.ExtruderManager.ExtruderManager.getInstance()
if print_information.materialLengths[index] != 0:
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
core_name = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]
if variant:
if variant.getName() != core_name:
Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName())
warnings.append(i18n_catalog.i18nc("@label", "Different print core (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1)))
for index in range(0, self._num_extruders):
# Check if there is enough material. Any failure in these results in a warning.
material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"]
if material_length != -1 and index < len(print_information.materialLengths) and print_information.materialLengths[index] > material_length:
Logger.log("w", "Printer reports that there is not enough material left for extruder %s. We need %s and the printer has %s", index + 1, print_information.materialLengths[index], material_length)
warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1))
material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
if material:
remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"]
if material.getMetaDataEntry("GUID") != remote_material_guid:
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1,
remote_material_guid,
material.getMetaDataEntry("GUID"))
# Check if the right cartridges are loaded. Any failure in these results in a warning.
extruder_manager = cura.Settings.ExtruderManager.ExtruderManager.getInstance()
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
core_name = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]
if variant:
if variant.getName() != core_name:
Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName())
warnings.append(i18n_catalog.i18nc("@label", "Different print core (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1)))
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True)
remote_material_name = "Unknown"
if remote_materials:
remote_material_name = remote_materials[0].getName()
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1))
material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
if material:
remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"]
if material.getMetaDataEntry("GUID") != remote_material_guid:
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1,
remote_material_guid,
material.getMetaDataEntry("GUID"))
try:
is_offset_calibrated = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["offset"]["state"] == "valid"
except KeyError: # Older versions of the API don't expose the offset property, so we must asume that all is well.
is_offset_calibrated = True
remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True)
remote_material_name = "Unknown"
if remote_materials:
remote_material_name = remote_materials[0].getName()
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1))
if not is_offset_calibrated:
warnings.append(i18n_catalog.i18nc("@label", "Print core {0} is not properly calibrated. XY calibration needs to be performed on the printer.").format(index + 1))
try:
is_offset_calibrated = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["offset"]["state"] == "valid"
except KeyError: # Older versions of the API don't expose the offset property, so we must asume that all is well.
is_offset_calibrated = True
if not is_offset_calibrated:
warnings.append(i18n_catalog.i18nc("@label", "Print core {0} is not properly calibrated. XY calibration needs to be performed on the printer.").format(index + 1))
else:
Logger.log("w", "There was no material usage found. No check to match used material with machine is done.")
if warnings:
text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
@ -712,7 +725,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
## Start requesting data from printer
def connect(self):
self.close() # Ensure that previous connection (if any) is killed.
if self.isConnected():
self.close() # Close previous connection
self._createNetworkManager()
@ -725,7 +739,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
## Check if this machine was authenticated before.
self._authentication_id = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_id", None)
self._authentication_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_key", None)
Logger.log("d", "Loaded authentication id %s from the metadata entry", self._authentication_id)
if self._authentication_id is None and self._authentication_key is None:
Logger.log("d", "No authentication found in metadata.")
else:
Logger.log("d", "Loaded authentication id %s and key %s from the metadata entry", self._authentication_id, self._getSafeAuthKey())
self._update_timer.start()
## Stop requesting data from printer
@ -787,19 +806,41 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
Logger.log("d", "Started sending g-code to remote printer.")
self._compressing_print = True
## Mash the data into single string
max_chars_per_line = 1024 * 1024 / 4 # 1 / 4 MB
byte_array_file_data = b""
batched_line = ""
def _compress_data_and_notify_qt(data_to_append):
compressed_data = gzip.compress(data_to_append.encode("utf-8"))
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Pretend that this is a response, as zipping might take a bit of time.
self._last_response_time = time()
return compressed_data
for line in self._gcode:
if not self._compressing_print:
self._progress_message.hide()
return # Stop trying to zip, abort was called.
if self._use_gzip:
byte_array_file_data += gzip.compress(line.encode("utf-8"))
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Pretend that this is a response, as zipping might take a bit of time.
self._last_response_time = time()
batched_line += line
# if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
# Compressing line by line in this case is extremely slow, so we need to batch them.
if len(batched_line) < max_chars_per_line:
continue
byte_array_file_data += _compress_data_and_notify_qt(batched_line)
batched_line = ""
else:
byte_array_file_data += line.encode("utf-8")
# don't miss the last batch if it's there
if self._use_gzip:
if batched_line:
byte_array_file_data += _compress_data_and_notify_qt(batched_line)
if self._use_gzip:
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
else:
@ -841,7 +882,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
## Check if the authentication request was allowed by the printer.
def _checkAuthentication(self):
Logger.log("d", "Checking if authentication is correct for id %s", self._authentication_id)
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id))))
## Request a authentication key from the printer so we can be authenticated
@ -849,8 +890,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
url = QUrl("http://" + self._address + self._api_prefix + "auth/request")
request = QNetworkRequest(url)
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
self.setAuthenticationState(AuthState.AuthenticationRequested)
self._authentication_key = None
self._authentication_id = None
self._manager.post(request, json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName()}).encode())
self.setAuthenticationState(AuthState.AuthenticationRequested)
## Send all material profiles to the printer.
def sendMaterialProfiles(self):
@ -920,7 +963,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
if status_code == 200:
if self._connection_state == ConnectionState.connecting:
self.setConnectionState(ConnectionState.connected)
self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8"))
try:
self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
return
self._spliceJSONData()
# Hide connection error message if the connection was restored
@ -932,7 +979,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
pass # TODO: Handle errors
elif "print_job" in reply_url: # Status update from print_job:
if status_code == 200:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
return
progress = json_data["progress"]
## If progress is 0 add a bit so another print can't be sent.
if progress == 0:
@ -1010,13 +1061,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
else:
global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
Application.getInstance().saveStack(global_container_stack) # Force save so we are sure the data is not lost.
Logger.log("i", "Authentication succeeded for id %s", self._authentication_id)
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
else: # Got a response that we didn't expect, so something went wrong.
Logger.log("e", "While trying to authenticate, we got an unexpected response: %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))
self.setAuthenticationState(AuthState.NotAuthenticated)
elif "auth/check" in reply_url: # Check if we are authenticated (user can refuse this!)
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
try:
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
return
if data.get("message", "") == "authorized":
Logger.log("i", "Authentication was approved")
self._verifyAuthentication() # Ensure that the verification is really used and correct.
@ -1029,8 +1084,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
elif reply.operation() == QNetworkAccessManager.PostOperation:
if "/auth/request" in reply_url:
# We got a response to requesting authentication.
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
try:
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
return
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack: # Remove any old data.
Logger.log("d", "Removing old network authentication data as a new one was requested.")
@ -1040,7 +1098,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
self._authentication_key = data["key"]
self._authentication_id = data["id"]
Logger.log("i", "Got a new authentication ID. Waiting for authorization: %s", self._authentication_id )
Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey())
# Check if the authentication is accepted.
self._checkAuthentication()
@ -1110,3 +1168,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
icon=QMessageBox.Question,
callback=callback
)
## Convenience function to "blur" out all but the last 5 characters of the auth key.
# This can be used to debug print the key, without it compromising the security.
def _getSafeAuthKey(self):
if self._authentication_key is not None:
result = self._authentication_key[-5:]
result = "********" + result
return result
return self._authentication_key

View file

@ -157,13 +157,15 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
for key in self._printers:
if key == active_machine.getMetaDataEntry("um_network_key"):
Logger.log("d", "Connecting [%s]..." % key)
self._printers[key].connect()
self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
if not self._printers[key].isConnected():
Logger.log("d", "Connecting [%s]..." % key)
self._printers[key].connect()
self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
else:
if self._printers[key].isConnected():
Logger.log("d", "Closing connection [%s]..." % key)
self._printers[key].close()
self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addPrinter(self, name, address, properties):
@ -181,9 +183,9 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
printer = self._printers.pop(name, None)
if printer:
if printer.isConnected():
printer.disconnect()
printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
printer.disconnect()
self.printerListChanged.emit()
## Handler for when the connection state of one of the detected printers changes

View file

@ -19,8 +19,8 @@ from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class USBPrinterOutputDevice(PrinterOutputDevice):
class USBPrinterOutputDevice(PrinterOutputDevice):
def __init__(self, serial_port):
super().__init__(serial_port)
self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
@ -148,6 +148,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Start a print based on a g-code.
# \param gcode_list List with gcode (strings).
def printGCode(self, gcode_list):
Logger.log("d", "Started printing g-code")
if self._progress or self._connection_state != ConnectionState.connected:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer is busy or not connected."))
self._error_message.show()
@ -183,6 +184,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Private function (threaded) that actually uploads the firmware.
def _updateFirmware(self):
Logger.log("d", "Attempting to update firmware")
self._error_code = 0
self.setProgress(0, 100)
self._firmware_update_finished = False
@ -202,6 +204,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
try:
programmer.connect(self._serial_port)
except Exception:
programmer.close()
pass
# Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
@ -312,8 +315,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
programmer.connect(self._serial_port) # Connect with the serial, if this succeeds, it's an arduino based usb device.
self._serial = programmer.leaveISP()
except ispBase.IspError as e:
programmer.close()
Logger.log("i", "Could not establish connection on %s: %s. Device is not arduino based." %(self._serial_port,str(e)))
except Exception as e:
programmer.close()
Logger.log("i", "Could not establish connection on %s, unknown reasons. Device is not arduino based." % self._serial_port)
# If the programmer connected, we know its an atmega based version.
@ -443,7 +448,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# This is ignored.
# \param filter_by_machine Whether to filter MIME types by machine. This
# is ignored.
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
# \param kwargs Keyword arguments.
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
container_stack = Application.getInstance().getGlobalContainerStack()
if container_stack.getProperty("machine_gcode_flavor", "value") == "UltiGCode":
@ -532,6 +538,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._sendNextGcodeLine()
elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with "resend" and "rs"
try:
Logger.log("d", "Got a resend response")
self._gcode_position = int(line.replace(b"N:",b" ").replace(b"N",b" ").replace(b":",b" ").split()[-1])
except:
if b"rs" in line:
@ -558,15 +565,20 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if ";" in line:
line = line[:line.find(";")]
line = line.strip()
# Don't send empty lines. But we do have to send something, so send
# m105 instead.
# Don't send the M0 or M1 to the machine, as M0 and M1 are handled as
# an LCD menu pause.
if line == "" or line == "M0" or line == "M1":
line = "M105"
try:
if line == "M0" or line == "M1":
line = "M105" # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
if ("G0" in line or "G1" in line) and "Z" in line:
z = float(re.search("Z([0-9\.]*)", line).group(1))
if self._current_z != z:
self._current_z = z
except Exception as e:
Logger.log("e", "Unexpected error with printer connection: %s" % e)
Logger.log("e", "Unexpected error with printer connection, could not parse current Z: %s: %s" % (e, line))
self._setErrorState("Unexpected error: %s" %e)
checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line)))
@ -665,4 +677,4 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def cancelPreheatBed(self):
Logger.log("i", "Cancelling pre-heating of the bed.")
self._setTargetBedTemperature(0)
self.preheatBedRemainingTimeChanged.emit()
self.preheatBedRemainingTimeChanged.emit()

View file

@ -236,8 +236,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
self.getOutputDeviceManager().removeOutputDevice(serial_port)
self.connectionStateChanged.emit()
except KeyError:
pass # no output device by this device_id found in connection list.
Logger.log("w", "Connection state of %s changed, but it was not found in the list")
@pyqtProperty(QObject , notify = connectionStateChanged)
def connectedPrinterList(self):

View file

@ -0,0 +1,190 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
import configparser #To check whether the appropriate exceptions are raised.
import pytest #To register tests with.
import VersionUpgrade24to25 #The module we're testing.
## Creates an instance of the upgrader to test with.
@pytest.fixture
def upgrader():
return VersionUpgrade24to25.VersionUpgrade24to25()
test_cfg_version_good_data = [
{
"test_name": "Simple",
"file_data": """[general]
version = 1
""",
"version": 1
},
{
"test_name": "Other Data Around",
"file_data": """[nonsense]
life = good
[general]
version = 3
[values]
layer_height = 0.12
infill_sparse_density = 42
""",
"version": 3
},
{
"test_name": "Negative Version", #Why not?
"file_data": """[general]
version = -20
""",
"version": -20
}
]
## Tests the technique that gets the version number from CFG files.
#
# \param data The parametrised data to test with. It contains a test name
# to debug with, the serialised contents of a CFG file and the correct
# version number in that CFG file.
# \param upgrader The instance of the upgrade class to test.
@pytest.mark.parametrize("data", test_cfg_version_good_data)
def test_cfgVersionGood(data, upgrader):
version = upgrader.getCfgVersion(data["file_data"])
assert version == data["version"]
test_cfg_version_bad_data = [
{
"test_name": "Empty",
"file_data": "",
"exception": configparser.Error #Explicitly not specified further which specific error we're getting, because that depends on the implementation of configparser.
},
{
"test_name": "No General",
"file_data": """[values]
layer_height = 0.1337
""",
"exception": configparser.Error
},
{
"test_name": "No Version",
"file_data": """[general]
true = false
""",
"exception": configparser.Error
},
{
"test_name": "Not a Number",
"file_data": """[general]
version = not-a-text-version-number
""",
"exception": ValueError
}
]
## Tests whether getting a version number from bad CFG files gives an
# exception.
#
# \param data The parametrised data to test with. It contains a test name
# to debug with, the serialised contents of a CFG file and the class of
# exception it needs to throw.
# \param upgrader The instance of the upgrader to test.
@pytest.mark.parametrize("data", test_cfg_version_bad_data)
def test_cfgVersionBad(data, upgrader):
with pytest.raises(data["exception"]):
upgrader.getCfgVersion(data["file_data"])
test_upgrade_preferences_removed_settings_data = [
{
"test_name": "Removed Setting",
"file_data": """[general]
visible_settings = baby;you;know;how;I;like;to;start_layers_at_same_position
""",
},
{
"test_name": "No Removed Setting",
"file_data": """[general]
visible_settings = baby;you;now;how;I;like;to;eat;chocolate;muffins
"""
},
{
"test_name": "No Visible Settings Key",
"file_data": """[general]
cura = cool
"""
},
{
"test_name": "No General Category",
"file_data": """[foos]
foo = bar
"""
}
]
## Tests whether the settings that should be removed are removed for the 2.5
# version of preferences.
@pytest.mark.parametrize("data", test_upgrade_preferences_removed_settings_data)
def test_upgradePreferencesRemovedSettings(data, upgrader):
#Get the settings from the original file.
original_parser = configparser.ConfigParser(interpolation = None)
original_parser.read_string(data["file_data"])
settings = set()
if original_parser.has_section("general") and "visible_settings" in original_parser["general"]:
settings = set(original_parser["general"]["visible_settings"].split(";"))
#Perform the upgrade.
_, upgraded_preferences = upgrader.upgradePreferences(data["file_data"], "<string>")
upgraded_preferences = upgraded_preferences[0]
#Find whether the removed setting is removed from the file now.
settings -= VersionUpgrade24to25._removed_settings
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(upgraded_preferences)
assert (parser.has_section("general") and "visible_settings" in parser["general"]) == (len(settings) > 0) #If there are settings, there must also be a preference.
if settings:
assert settings == set(parser["general"]["visible_settings"].split(";"))
test_upgrade_instance_container_removed_settings_data = [
{
"test_name": "Removed Setting",
"file_data": """[values]
layer_height = 0.1337
start_layers_at_same_position = True
"""
},
{
"test_name": "No Removed Setting",
"file_data": """[values]
oceans_number = 11
"""
},
{
"test_name": "No Values Category",
"file_data": """[general]
type = instance_container
"""
}
]
## Tests whether the settings that should be removed are removed for the 2.5
# version of instance containers.
@pytest.mark.parametrize("data", test_upgrade_instance_container_removed_settings_data)
def test_upgradeInstanceContainerRemovedSettings(data, upgrader):
#Get the settings from the original file.
original_parser = configparser.ConfigParser(interpolation = None)
original_parser.read_string(data["file_data"])
settings = set()
if original_parser.has_section("values"):
settings = set(original_parser["values"])
#Perform the upgrade.
_, upgraded_container = upgrader.upgradeInstanceContainer(data["file_data"], "<string>")
upgraded_container = upgraded_container[0]
#Find whether the forbidden setting is still in the container.
settings -= VersionUpgrade24to25._removed_settings
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(upgraded_container)
assert parser.has_section("values") == (len(settings) > 0) #If there are settings, there must also be the values category.
if settings:
assert settings == set(parser["values"])

View file

@ -3,6 +3,7 @@
import copy
import io
from typing import Optional
import xml.etree.ElementTree as ET
from UM.Resources import Resources
@ -11,7 +12,7 @@ from UM.Util import parseBool
from cura.CuraApplication import CuraApplication
import UM.Dictionary
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.InstanceContainer import InstanceContainer, InvalidInstanceError
from UM.Settings.ContainerRegistry import ContainerRegistry
## Handles serializing and deserializing material containers from an XML file
@ -118,6 +119,7 @@ class XmlMaterialProfile(InstanceContainer):
metadata.pop("variant", "")
metadata.pop("type", "")
metadata.pop("base_file", "")
metadata.pop("approximate_diameter", "")
## Begin Name Block
builder.start("name")
@ -369,8 +371,30 @@ class XmlMaterialProfile(InstanceContainer):
self._dirty = False
self._path = ""
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
return "material"
def getVersionFromSerialized(self, serialized: str) -> Optional[int]:
version = None
data = ET.fromstring(serialized)
metadata = data.iterfind("./um:metadata/*", self.__namespaces)
for entry in metadata:
tag_name = _tag_without_namespace(entry)
if tag_name == "version":
try:
version = int(entry.text)
except Exception as e:
raise InvalidInstanceError("Invalid version string '%s': %s" % (entry.text, e))
break
if version is None:
raise InvalidInstanceError("Missing version in metadata")
return version
## Overridden from InstanceContainer
def deserialize(self, serialized):
# update the serialized data first
from UM.Settings.Interfaces import ContainerInterface
serialized = ContainerInterface.deserialize(self, serialized)
data = ET.fromstring(serialized)
# Reset previous metadata
@ -405,10 +429,10 @@ class XmlMaterialProfile(InstanceContainer):
continue
meta_data[tag_name] = entry.text
if not "description" in meta_data:
if "description" not in meta_data:
meta_data["description"] = ""
if not "adhesion_info" in meta_data:
if "adhesion_info" not in meta_data:
meta_data["adhesion_info"] = ""
property_values = {}
@ -437,6 +461,7 @@ class XmlMaterialProfile(InstanceContainer):
Logger.log("d", "Unsupported material setting %s", key)
self._cached_values = global_setting_values
meta_data["approximate_diameter"] = round(diameter)
meta_data["compatible"] = global_compatibility
self.setMetaData(meta_data)
self._dirty = False
@ -581,7 +606,8 @@ class XmlMaterialProfile(InstanceContainer):
"Ultimaker 2 Extended": "ultimaker2_extended",
"Ultimaker 2 Extended+": "ultimaker2_extended_plus",
"Ultimaker Original": "ultimaker_original",
"Ultimaker Original+": "ultimaker_original_plus"
"Ultimaker Original+": "ultimaker_original_plus",
"IMADE3D JellyBOX": "imade3d_jellybox"
}
# Map of recognised namespaces with a proper prefix.