mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-05 16:51:12 -07:00
Merge branch 'master' into themed-layer-view
This commit is contained in:
commit
2493bd6fe6
289 changed files with 90651 additions and 67915 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,54 @@
|
|||
[2.5.0]
|
||||
*Speed.
|
||||
We’ve 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
|
||||
We’ve 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. How’s 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? We’ve 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. We’ve 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, you’ll 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
|
||||
We’ve 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. It’s 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; we’ve now amended this.
|
||||
File location after restart: The old version of Cura didn’t remember the last opened file location after it’s 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. We’ve 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: We’ve enlarged the summary box when saving your project.
|
||||
Cubic subdivision infill: In the past, the cubic subdivision infill sometimes didn’t 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. It’s 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; we’ve now amended this.
|
||||
- File location after restart: The old version of Cura didn’t remember the last opened file location after it’s 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. We’ve 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: We’ve enlarged the summary box when saving the project.
|
||||
- Cubic subdivision infill: In the past, the cubic subdivision infill sometimes didn’t 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
193
plugins/GCodeReader/GCodeReader.py
Normal file → Executable 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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
213
plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py
Normal file → Executable 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue