Added support for CuraEngine 15.06

This commit is contained in:
Nicanor Romero Venier 2015-06-29 09:35:26 +02:00 committed by Gina Häußge
parent c2eb8828de
commit fd788e737f
3 changed files with 1645 additions and 74 deletions

View file

@ -19,6 +19,8 @@ import octoprint.settings
from octoprint.util.paths import normalize as normalize_path
from .profile import Profile
from .profile import GcodeFlavors
from .profile import parse_gcode_flavor
class CuraPlugin(octoprint.plugin.SlicerPlugin,
octoprint.plugin.SettingsPlugin,
@ -230,15 +232,29 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
self._cura_logger.info(u"### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path))
engine_settings = self._convert_to_engine(profile_path, printer_profile, posX, posY)
executable = normalize_path(self._settings.get(["cura_engine"]))
if not executable:
return False, "Path to CuraEngine is not configured "
working_dir, _ = os.path.split(executable)
working_dir = os.path.dirname(executable)
# Start building the argument list for the CuraEngine command execution
args = [executable, '-v', '-p']
for k, v in engine_settings.items():
# Get the CuraEngine version out of the "usage" line that is gotten when calling the CuraEngine with the -h argument
new_engine = self._is_new_engine_version(executable)
# If the CuraEngine is the new version, add the JSON file path as argument
if new_engine:
args += ['-j', os.path.join(self._basefolder, "profiles", "fdmprinter.json")]
profile = Profile(self._load_profile(profile_path), printer_profile, posX, posY)
engine_settings = self._convert_to_engine(profile, new_engine=new_engine)
# Add the settings (sorted alphabetically) to the command
for k, v in sorted(engine_settings.items(), key=lambda s: s[0]):
args += ["-s", "%s=%s" % (k, str(v))]
args += ["-o", machinecode_path, model_path]
@ -321,6 +337,8 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
except:
pass
# Get the filament usage
elif line.startswith(u"Filament:") or line.startswith(u"Filament2:"):
if line.startswith(u"Filament:"):
filament_str = line[len(u"Filament:"):].strip()
@ -331,17 +349,25 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
try:
filament = int(filament_str)
if analysis is None:
analysis = dict()
if not "filament" in analysis:
analysis["filament"] = dict()
if not tool_key in analysis["filament"]:
analysis["filament"][tool_key] = dict()
analysis["filament"][tool_key]["length"] = filament
if "filamentDiameter" in engine_settings:
radius_in_cm = float(int(engine_settings["filamentDiameter"]) / 10000.0) / 2.0
filament_in_cm = filament / 10.0
analysis["filament"][tool_key]["volume"] = filament_in_cm * math.pi * radius_in_cm * radius_in_cm
if profile.get_float("filament_diameter") != None:
# The new CuraEngine outputs the "Filament" value as volume independently from the Gcode flavor
if new_engine:
analysis["filament"][tool_key] = _get_usage_from_volume(filament, profile.get_float("filament_diameter"))
# The old CuraEngine outputs the "Filament" value as volume only for ULTIGCODE and REPRAP_VOLUME
else:
if profile.get("gcode_flavor") == GcodeFlavors.ULTIGCODE or profile.get("gcode_flavor") == GcodeFlavors.REPRAP_VOLUME:
analysis["filament"][tool_key] = _get_usage_from_volume(filament, profile.get_float("filament_diameter"))
else:
analysis["filament"][tool_key] = _get_usage_from_length(filament, profile.get_float("filament_diameter"))
except:
pass
finally:
@ -391,6 +417,11 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
profile_dict = yaml.safe_load(f)
except:
raise IOError("Couldn't read profile from {path}".format(path=path))
if "gcode_flavor" in profile_dict and not isinstance(profile_dict["gcode_flavor"], (list, tuple)):
profile_dict["gcode_flavor"] = parse_gcode_flavor(profile_dict["gcode_flavor"])
self._save_profile(path, profile_dict)
return profile_dict
def _save_profile(self, path, profile, allow_overwrite=True):
@ -398,9 +429,28 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
with open(path, "wb") as f:
yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True)
def _convert_to_engine(self, profile_path, printer_profile, posX, posY):
profile = Profile(self._load_profile(profile_path), printer_profile, posX, posY)
return profile.convert_to_engine()
def _convert_to_engine(self, profile, new_engine):
if new_engine:
return profile.convert_to_new_engine()
else:
return profile.convert_to_engine()
def _is_new_engine_version(self, executable):
import sarge
command = [executable, "-j", os.path.join(self._basefolder, "profiles", "fdmprinter.json")]
p = sarge.run(command, stdout=sarge.Capture(), stderr=sarge.Capture())
if p.returncode != 0:
self._logger.warn("Could not run {} -j, returned {}".format(executable, p.returncode))
self._logger.info(u"Assuming pre 15.06 version of CuraEngine.")
return False
for line in p.stderr.read().split('\n'):
if "unknown option: j" in line.strip().lower():
self._logger.info(u"Using pre 15.06 version of CuraEngine.")
return False
self._logger.info(u"Using post 15.06 version of CuraEngine.")
return True
def _sanitize_name(name):
if name is None:
@ -415,9 +465,39 @@ def _sanitize_name(name):
sanitized_name = sanitized_name.replace(" ", "_")
return sanitized_name.lower()
def _get_usage_from_volume(filament_volume, filament_diameter):
# filament_volume is expressed in mm^3
# usage["volume"] is in cm^3 and usage["length"] is in mm
usage = dict()
usage["volume"] = filament_volume / 1000.0
radius_in_mm = filament_diameter / 2.0
usage["length"] = filament_volume / (math.pi * radius_in_mm * radius_in_mm)
return usage
def _get_usage_from_length(filament_length, filament_diameter):
# filament_length is expressed in mm
# usage["volume"] is in cm^3 and usage["length"] is in mm
usage = dict()
usage["length"] = filament_length
radius_in_cm = (filament_diameter / 10.0) / 2.0
length_in_cm = filament_length / 10.0
usage["volume"] = length_in_cm * math.pi * radius_in_cm * radius_in_cm
return usage
__plugin_name__ = "CuraEngine"
__plugin_author__ = "Gina Häußge"
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura"
__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint"
__plugin_license__ = "AGPLv3"
__plugin_implementation__ = CuraPlugin()

View file

@ -32,12 +32,12 @@ class MachineShapeTypes(object):
CIRCULAR = "circular"
class GcodeFlavors(object):
REPRAP = "reprap"
REPRAP_VOLUME = "reprap_volume"
ULTIGCODE = "ultigcode"
MAKERBOT = "makerbot"
BFB = "bfb"
MACH3 = "mach3"
REPRAP = (0, "reprap")
ULTIGCODE = (1, "ultigcode")
MAKERBOT = (2, "makerbot")
BFB = (3, "bfb")
MACH3 = (4, "mach3")
REPRAP_VOLUME = (5, "reprap_volume")
defaults = dict(
@ -393,15 +393,45 @@ class Profile(object):
}
)
# Translation dict for new CuraEngine option names
new_engine_options = dict(
fill_sparse_density="fill_density",
machine_nozzle_size="nozzle_size",
material_print_temperature="print_temperature",
#support_type="support", # In conflict with "support_type" from old CuraEngine
support_pattern="support_type",
adhesion_type="platform_adhesion",
material_diameter="filament_diameter",
material_flow="filament_flow",
speed_travel="travel_speed",
speed_layer_0="bottom_layer_speed",
speed_print="print_speed",
speed_infill="infill_speed",
speed_wall_0="outer_shell_speed",
speed_wall_x="inner_shell_speed",
cool_fan_enabled="fan_enabled",
cool_fan_full_at_height="fan_full_height",
cool_fan_speed="fan_speed",
cool_fan_speed_max="fan_speed_max",
cool_lift_head="cool_head_lift",
cool_min_speed="cool_min_feedrate",
machine_start_gcode="start_gcode",
machine_end_gcode="end_gcode",
machine_gcode_flavor="gcode_flavor")
result = dict()
for section in config.sections():
if not section in ("profile", "alterations"):
if not section in ("profile", "alterations", "machine"):
continue
for option in config.options(section):
ignored = False
key = option
if section == "machine" and key != "gcode_flavor":
continue
# try to fetch the value in the correct type
try:
value = config.getboolean(section, option)
@ -438,9 +468,16 @@ class Profile(object):
# if the key has to be translated to a new value, do that now
key = translated_options[key]
if key in new_engine_options:
# if the key has to be translated from the new CuraEngine versions, do that now
key = new_engine_options[key]
if key in value_conversions and value in value_conversions[key]:
value = value_conversions[key][value]
if key == "gcode_flavor":
value = parse_gcode_flavor(value)
if index is not None:
# if we have an array to fill, make sure the target array exists and has the right size
if not key in result:
@ -710,61 +747,70 @@ class Profile(object):
return pre + str(f)
def get_gcode(self, key):
extruder_count = self.get_int("extruder_amount")
def get_gcode(self, key, new_engine=False):
prefix = ""
postfix = ""
if self.get("gcode_flavor") == GcodeFlavors.ULTIGCODE:
if key == "end_gcode":
return "M25 ;Stop reading from this point on.\n;CURA_PROFILE_STRING:%s\n" % (self.get_profile_string())
return "M25 ;Stop reading from this point on.\n;CURA_OCTO_PROFILE_STRING:%s\n" % (self.get_profile_string())
return ""
if key == "start_gcode":
contents = self.get_gcode_template("start_gcode")
e_steps = self.get_float("steps_per_e")
if e_steps > 0:
prefix += "M92 E{e_steps}\n" % (e_steps)
temp = self.get_float("print_temperature")
bed_temp = 0
if self.get_boolean("has_heated_bed"):
bed_temp = self.get_float("print_bed_temperature")
include_bed_temp = bed_temp > 0 and not "{print_bed_temperature}" in Profile.regex_strip_comments.sub("", contents)
if include_bed_temp:
prefix += "M140 S{bed_temp}\n".format(bed_temp=bed_temp)
if temp > 0 and not "{print_temperature}" in Profile.regex_strip_comments.sub("", contents):
if extruder_count > 0:
def temp_line(temp, extruder, template):
t = temp
if extruder > 0:
print_temp = self.get_float("print_temperature%d" % (extruder + 1))
if print_temp > 0:
t = print_temp
return template.format(extruder=extruder, temp=t)
prefix_preheat = ""
prefix_waitheat = ""
for n in xrange(0, extruder_count):
if n > 0:
prefix_preheat += temp_line(temp, n, "M104 T{extruder} S{temp}\n")
prefix_waitheat += temp_line(temp, n, "M109 T{extruder} S{temp}\n")
prefix += prefix_preheat + prefix_waitheat + "T0\n"
else:
prefix += "M109 S{temp}\n".format(temp=temp)
if include_bed_temp:
prefix += "M190 S{bed_temp}\n".format(bed_temp=bed_temp)
# If using the new CuraEngine, there is no need to extend the start_gcode
if not new_engine:
prefix += self.get_start_gcode_prefix(contents)
else:
contents = self.get_gcode_template(key)
return unicode(prefix + re.sub("(.)\{([^\}]*)\}", self.replaceTagMatch, contents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n'
def get_start_gcode_prefix(self, contents):
extruder_count = self.get_int("extruder_amount")
prefix = ""
e_steps = self.get_float("steps_per_e")
if e_steps > 0:
prefix += "M92 E{e_steps}\n" % (e_steps)
temp = self.get_float("print_temperature")
bed_temp = 0
if self.get_boolean("has_heated_bed"):
bed_temp = self.get_float("print_bed_temperature")
include_bed_temp = bed_temp > 0 and not "{print_bed_temperature}" in Profile.regex_strip_comments.sub("", contents)
if include_bed_temp:
prefix += "M140 S{bed_temp}\n".format(bed_temp=bed_temp)
if temp > 0 and not "{print_temperature}" in Profile.regex_strip_comments.sub("", contents):
if extruder_count > 0:
def temp_line(temp, extruder, template):
t = temp
if extruder > 0:
print_temp = self.get_float("print_temperature%d" % (extruder + 1))
if print_temp > 0:
t = print_temp
return template.format(extruder=extruder, temp=t)
prefix_preheat = ""
prefix_waitheat = ""
for n in xrange(0, extruder_count):
if n > 0:
prefix_preheat += temp_line(temp, n, "M104 T{extruder} S{temp}\n")
prefix_waitheat += temp_line(temp, n, "M109 T{extruder} S{temp}\n")
prefix += prefix_preheat + prefix_waitheat + "T0\n"
else:
prefix += "M109 S{temp}\n".format(temp=temp)
if include_bed_temp:
prefix += "M190 S{bed_temp}\n".format(bed_temp=bed_temp)
return prefix
def calculate_edge_width_and_line_count(self):
wall_thickness = self.get_float("wall_thickness")
nozzle_size = self._printer_profile["extruder"]["nozzleDiameter"]
@ -810,20 +856,20 @@ class Profile(object):
def get_pos_x(self):
if self._posX:
try:
return int(float(self._posX) * 1000)
return int(float(self._posX))
except ValueError:
pass
return int(self.get_float("machine_width") / 2.0 * 1000) if not self.get_boolean("machine_center_is_zero") else 0.0
return int(self.get_float("machine_width") / 2.0 ) if not self.get_boolean("machine_center_is_zero") else 0.0
def get_pos_y(self):
if self._posY:
try:
return int(float(self._posY) * 1000)
return int(float(self._posY))
except ValueError:
pass
return int(self.get_float("machine_depth") / 2.0 * 1000) if not self.get_boolean("machine_center_is_zero") else 0.0
return int(self.get_float("machine_depth") / 2.0) if not self.get_boolean("machine_center_is_zero") else 0.0
def convert_to_engine(self):
@ -873,8 +919,8 @@ class Profile(object):
"coolHeadLift": 1 if self.get_boolean("cool_head_lift") else 0,
# model positioning
"posx": self.get_pos_x(),
"posy": self.get_pos_y(),
"posx": self.get_pos_x() * 1000, # in microns
"posy": self.get_pos_y() * 1000, # in microns
# gcodes
"startCode": self.get_gcode("start_gcode"),
@ -953,16 +999,7 @@ class Profile(object):
settings["layerThickness"] = 1000
# gcode flavor
if self.get("gcode_flavor") == GcodeFlavors.ULTIGCODE:
settings["gcodeFlavor"] = 1
elif self.get("gcode_flavor") == GcodeFlavors.MAKERBOT:
settings["gcodeFlavor"] = 2
elif self.get("gcode_flavor") == GcodeFlavors.BFB:
settings["gcodeFlavor"] = 3
elif self.get("gcode_flavor") == GcodeFlavors.MACH3:
settings["gcodeFlavor"] = 4
elif self.get("gcode_flavor") == GcodeFlavors.REPRAP_VOLUME:
settings["gcodeFlavor"] = 5
settings["gcodeFlavor"] = self.get("gcode_flavor")[0]
# extras
if self.get_boolean("spiralize"):
@ -978,3 +1015,132 @@ class Profile(object):
settings["enableOozeShield"] = 1
return settings
def convert_to_new_engine(self):
settings = {
"layer_height": self.get_float("layer_height"),
"wall_thickness": self.get_float("wall_thickness"),
"retraction_enable": self.get_boolean("retraction_enable"),
"top_bottom_thickness": self.get_float("solid_layer_thickness"),
"fill_sparse_density": self.get_float("fill_density"),
"machine_nozzle_size": self.get_float("nozzle_size"),
"speed_print": self.get_float("print_speed"),
"material_print_temperature": self.get_float("print_temperature"),
"material_diameter": self.get_float("filament_diameter"),
"material_flow": self.get_float("filament_flow"),
"retraction_speed": self.get_float("retraction_speed"),
"retraction_amount": self.get_float("retraction_amount") if self.get_boolean("retraction_enable") else 0,
"retraction_min_travel": self.get_float("retraction_min_travel"),
"retraction_combing": self.get_boolean("retraction_combing"),
"retraction_hop": self.get_float("retraction_hop"),
"bottom_thickness": self.get_float("bottom_thickness"),
"top_layers": self.calculate_solid_layer_count(),
"bottom_layers": self.calculate_solid_layer_count(),
"speed_travel": self.get_float("travel_speed"),
"speed_layer_0": self.get_float("bottom_layer_speed"),
"speed_infill": self.get_float("infill_speed") if self.get_float("infill_speed") > 0 else self.get_float("print_speed"),
"speed_wall_0": self.get_float("outer_shell_speed") if self.get_float("outer_shell_speed") > 0 else self.get_float("print_speed"), # Translated from "inset0_speed"
"speed_wall_x": self.get_float("inner_shell_speed") if self.get_float("inner_shell_speed") > 0 else self.get_float("print_speed"), # Translated from "insetx_speed"
"cool_min_layer_time": self.get_float("cool_min_layer_time"),
"cool_fan_enabled": self.get_boolean("fan_enabled"),
"cool_fan_full_at_height": self.get_int("fan_full_height"),
"cool_fan_speed": self.get_float("fan_speed"),
"cool_fan_speed_max": self.get_float("fan_speed_max") if self.get_boolean("fan_enabled") else 0,
"cool_lift_head": self.get_boolean("cool_head_lift"),
"cool_min_speed": self.get_float("cool_min_feedrate"),
"skirt_line_count": self.get_int("skirt_line_count"),
"fill_overlap": self.get_float("fill_overlap"),
"magic_spiralize": self.get_boolean("spiralize"),
# machine settings
"machine_width": self.get_float("machine_width"),
"machine_depth": self.get_float("machine_depth"),
"machine_center_is_zero": self.get_boolean("machine_center_is_zero"),
"machine_heated_bed": self.get_boolean("has_heated_bed"),
# gcodes
"machine_start_gcode": self.get_gcode("start_gcode", new_engine=True),
"machine_end_gcode": self.get_gcode("end_gcode", new_engine=True),
}
# gcode flavor
settings["machine_gcode_flavor"] = self.get("gcode_flavor")[0]
# heated bed - The new CuraEngine uses the integer "material_bed_temperature" instead of the boolean "machine_heated_bed"
if self.get_boolean("has_heated_bed"):
settings["material_bed_temperature"] = self.get_float("print_bed_temperature")
else:
settings["material_bed_temperature"] = 0
# support
if self.get("support") == SupportLocationTypes.NONE:
settings["support_enable"] = False
else:
settings["support_enable"] = True
settings["support_angle"] = self.get_float("support_angle")
settings["support_fill_rate"] = self.get_float("support_fill_rate")
settings["support_xy_distance"] = self.get_float("support_xy_distance")
settings["support_z_distance"] = self.get_float("support_z_distance")
# support type
if self.get("support") == SupportLocationTypes.TOUCHING_BUILDPLATE:
settings["support_type"] = "Touching Buildplate"
elif self.get("support") == SupportLocationTypes.EVERYWHERE:
settings["support_type"] = "Everywhere"
# support pattern
if self.get("support_type") == SupportTypes.GRID:
settings["support_pattern"] = "Grid"
elif self.get("support_type") == SupportTypes.LINES:
settings["support_pattern"] = "Lines"
# adhesion type
if self.get("platform_adhesion") == PlatformAdhesionTypes.NONE:
settings["adhesion_type"] = "None"
elif self.get("platform_adhesion") == PlatformAdhesionTypes.BRIM:
settings["adhesion_type"] = "Brim"
settings["brim_line_count"] = self.get_int("brim_line_count")
elif self.get("platform_adhesion") == PlatformAdhesionTypes.RAFT:
settings["adhesion_type"] = "Raft"
settings["raft_margin"] = self.get_int("raft_margin")
settings["raft_line_spacing"] = self.get_float("raft_line_spacing")
settings["raft_base_thickness"] = self.get_float("raft_base_thickness")
settings["raft_base_linewidth"] = self.get_float("raft_base_linewidth")
settings["raft_interface_thickness"] = self.get_float("raft_interface_thickness")
settings["raft_interface_linewidth"] = self.get_float("raft_interface_linewidth")
settings["raft_airgap"] = self.get_float("raft_airgap")
settings["raft_surface_layers"] = self.get_int("raft_surface_layers")
# skirt
if self.get_int("skirt_line_count") != 0:
settings["skirt_gap"] = self.get_float("skirt_gap")
settings["skirt_minimal_length"] = self.get_float("skirt_minimal_length")
return settings
def parse_gcode_flavor(value):
value = value.lower()
if "reprap" in value and ("volume" in value or "volumatric" in value):
return GcodeFlavors.REPRAP_VOLUME
elif "ultigcode" in value:
return GcodeFlavors.ULTIGCODE
elif "makerbot" in value:
return GcodeFlavors.MAKERBOT
elif "bfb" in value:
return GcodeFlavors.BFB
elif "mach3" in value:
return GcodeFlavors.MACH3
else:
return GcodeFlavors.REPRAP

File diff suppressed because it is too large Load diff