From 5bcb4e9554cf092f5e826e48ec8d325613439b3e Mon Sep 17 00:00:00 2001 From: TinkerGnome Date: Sat, 23 Mar 2019 19:49:48 +0100 Subject: [PATCH 1/3] add Mark2 definition files --- .../scripts/Mark2Tweaks.py | 312 ++++++++++++++++++ .../definitions/Mark2_for_Ultimaker2.def.json | 241 ++++++++++++++ resources/extruders/Mark2_extruder1.def.json | 19 ++ resources/extruders/Mark2_extruder2.def.json | 19 ++ .../images/Mark2_for_Ultimaker2_backplate.png | Bin 0 -> 13655 bytes .../Mark2_for_Ultimaker2_0.25.inst.cfg | 19 ++ .../Mark2_for_Ultimaker2_0.4.inst.cfg | 17 + .../Mark2_for_Ultimaker2_0.6.inst.cfg | 18 + .../Mark2_for_Ultimaker2_0.8.inst.cfg | 18 + 9 files changed, 663 insertions(+) create mode 100644 plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py create mode 100644 resources/definitions/Mark2_for_Ultimaker2.def.json create mode 100644 resources/extruders/Mark2_extruder1.def.json create mode 100644 resources/extruders/Mark2_extruder2.def.json create mode 100644 resources/images/Mark2_for_Ultimaker2_backplate.png create mode 100644 resources/variants/Mark2_for_Ultimaker2_0.25.inst.cfg create mode 100644 resources/variants/Mark2_for_Ultimaker2_0.4.inst.cfg create mode 100644 resources/variants/Mark2_for_Ultimaker2_0.6.inst.cfg create mode 100644 resources/variants/Mark2_for_Ultimaker2_0.8.inst.cfg diff --git a/plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py b/plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py new file mode 100644 index 0000000000..bc9168e931 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py @@ -0,0 +1,312 @@ +## +## Mark2Plugin - Mark2Tweaks: Cura PostProcessingPlugin script for the Mark 2. +## Copyright (C) 2016,2017 Krys Lawrence +## +## This program is free software: you can redistribute it and/or modify +## it under the terms of the GNU Affero General Public License as +## published by the Free Software Foundation, either version 3 of the +## License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Affero General Public License for more details. +## +## You should have received a copy of the GNU Affero General Public License +## along with this program. If not, see . +## + +"""Add-on script for nallath's PostProcessingPlugin to help the Mark 2. + +See https://github.com/nallath/PostProcessingPlugin for details about the +plugin. + +Put this script in PostProcessingPlugin's scripts folder. +""" + + +import re +import traceback +import contextlib +from UM.Logger import Logger +from ..Script import Script + + +"""Convenience alias for UM.Logger.Logger.log.""" +log = Logger.log + + +@contextlib.contextmanager +def exception_handler(layer_num, log_not_raise=False): + """Either raise or just log the last exception.""" + try: + yield + except: + if log_not_raise: + layer_log(layer_num, 'e', ''.join(traceback.format_exc())) + else: + layer_log(layer_num, 'e', 'Exception! Traceback follows.') + raise + + +def layer_log(layer_num, message_type, message): + """Log a message prefixed with the curent layer number.""" + log(message_type, 'Layer {:.0f}: {}'.format(layer_num, message)) + + +class Mark2Tweaks(Script): + """Optimize the G-code output for the Mark 2.""" + + def getSettingDataString(self): + """Return script identification and GUI options.""" + # Note: The "version" key is not this script's version, but the API + # version of the PostProcessingPlugin. + return '''\ + { + "name": "Mark 2 Tweaks", + "key": "Mark2Tweaks", + "metadata": {}, + "version": 2, + "settings": { + "remove_hack": { + "label": "Clean Up Cura Workaround", + "description": + "The Mark 2 settings include a workaround to a Cura limitation. This tweak cleans up after it.", + "type": "bool", + "default_value": true + }, + "remove_superfluous": { + "label": "Remove Extra Movements", + "description": + "Remove superfluous movements after each tool change. This can improve print quality by preventing materials from incorrectly touching immediately after a tool change.", + "type": "bool", + "default_value": true + }, + "ignore_errors": { + "label": "Ignore Errors", + "description": + "If any errors occur while performing the tweaks, skip them keep going", + "type": "bool", + "default_value": true + } + } + }''' + + def execute(self, data): + """Process all G-code and apply selected tweaks.""" + log('d', '*** MARK 2 TWEAKS START ***') + remove_hack = self.getSettingValueByKey('remove_hack') + remove_superfluous = self.getSettingValueByKey('remove_superfluous') + ignore_errors = self.getSettingValueByKey('ignore_errors') + log('d', 'Remove Hack: {}'.format(remove_hack)) + log('d', 'Remove Superfluous: {}'.format(remove_superfluous)) + log('d', 'Ignore Errors: {}'.format(ignore_errors)) + for layer_idx, layer in enumerate(data): + lines = layer.split('\n') + layer_num = self.find_layer_num(lines) + if layer_num is None: + continue + # Copy of lines so lines can be deleted or inserted in loop + for line_idx, line in enumerate(lines[:]): + if not line in ('T0', 'T1'): + continue + if remove_hack: + with exception_handler(layer_num, ignore_errors): + self.remove_hack(layer_num, lines, line_idx) + if remove_superfluous: + with exception_handler(layer_num, ignore_errors): + self.remove_superfluous(layer_num, lines, line_idx) + data[layer_idx] = '\n'.join(lines) + log('d', '*** MARK 2 TWEAKS END ***') + return data + + def find_layer_num(self, lines): + """Return the current layer number as a float.""" + result = self.find_line(lines, ';LAYER:', whole=False) + if result is not None: + return self.getValue(result, ";LAYER:") + + def remove_hack(self, layer_num, lines, t_idx): + """Remove TinkerGnome's Cura print area workaround line. + + If there is a G0 between T and G10/M104, remove it. + There should only be one. + TinkerGnome says adding it was a hack/workaround so we can kill it. + """ + end_idx = self.find_g10_or_m104(layer_num, lines, t_idx) + hack = self.find_line_and_index(lines, 'G0', ('X', 'Y', 'Z'), t_idx, + end_idx) + if hack is None: + return + hack_line, hack_idx = hack + if (self.getValue(hack_line, 'Z') == 14 + and self.getValue(hack_line, 'Y') == 35): + layer_log(layer_num, 'd', 'Striping Cura print area hack.') + del lines[hack_idx] + + def remove_superfluous(self, layer_num, lines, t_idx): + """Collapse any post tool change movents into a single movement. + + Any non-extrusion movements after tool change should be collapsed into + a single line. Keep only the last G0/G1 but add the F and Z of the + first G0/G1. But only collapse if there is more than one line. + """ + start_idx = self.find_g10_or_m104(layer_num, lines, t_idx) + end_idx = self.find_line_index(lines, ('G0', 'G1'), 'E', start_idx) + assert end_idx is not None, \ + 'Cannot find extruding G0/G1 line after tool change.' + + first_g = self.find_line_and_index(lines, ('G0', 'G1'), None, + start_idx, end_idx) + assert first_g is not None, \ + 'Sanity Check: Could not find a G0/G1 line before extrusion and ' \ + 'after tool change.' + first_g_line, first_g_idx = first_g + assert first_g_idx < end_idx, \ + 'Sanity Check: First G0/G1 is >= to first extrusion.' + + f_value = self.getValue(first_g_line, 'F') + z_value = self.getValue(first_g_line, 'Z') + assert z_value is not None, \ + 'Sanity Check: Z value not found in first G0/G1 line.' + + self.delete_all_g0_or_g1_except_last(layer_num, lines, first_g_idx, + 'Collapsing post tool change movements.') + assert self.is_g0_or_g1(lines[first_g_idx]), \ + 'Sanity Check: Missing G0/G1 after collapse.' + assert not self.is_g0_or_g1(lines[first_g_idx+1]), \ + 'Sanity Check: More than one G0/G1 after collapse.' + + self.add_f_and_z_values(layer_num, lines, first_g_idx, z_value, + f_value) + assert self.getValue(lines[first_g_idx], 'Z') is not None, \ + 'Sanity Check: Missing required Z value.' + + def find_g10_or_m104(self, layer_num, lines, t_idx): + """Find the next G10 or M104 line. + + G10 is for UltiGCode-style G-code. + M104 is for RepRap-style G-code. + """ + idx = self.find_line_index(lines, 'G10', start=t_idx) + if idx is None: # Assume RepRap style G-code + idx = self.find_line_index(lines, 'M104', start=t_idx) + assert idx is not None, \ + 'Cannot find G10/M104 after tool change.' + assert t_idx < idx < t_idx + 10, \ + 'Sanity Check: G10/M104 too far from T' + return idx + + def delete_all_g0_or_g1_except_last(self, layer_num, lines, first_g_idx, + log_msg): + """Delete all G0/G1 lines, except the last one. + + As long as there is more than one G line, delete the first. + Subsequent G line indices move up by one == first_g_idx. + This works only if lines are deleted and not just replaced. + If only one G, never run. Last G is not deleted. + + Also log only once if one or more deletes occurs. + """ + has_logged = False + while self.is_g0_or_g1(lines[first_g_idx+1]): + if not has_logged: + # Never log on single line. Only log once if multiple lines. + layer_log(layer_num, 'd', log_msg) + has_logged = True + del lines[first_g_idx] + + def is_g0_or_g1(self, line): + """Return true is line is a G0 or G1 command.""" + return line.startswith('G0 ') or line.startswith('G1 ') + + def add_f_and_z_values(self, layer_num, lines, g_idx, z_value, + f_value=None): + """Add Z and F values to the indicated G0/G1 line. + + f_value is optional. + Existing Z and F values will not be replaced. + """ + line = lines[g_idx] + fields = line.split(' ') + if f_value is not None and self.getValue(line, 'F') is None: + fields.insert(1, 'F{}'.format(f_value)) + if self.getValue(line, 'Z') is None: + fields.append('Z{}'.format(z_value)) + lines[g_idx] = ' '.join(fields) + + def find_line(self, *args, **kwargs): + """Return just the line from self.find_line_and_index().""" + result = self.find_line_and_index(*args, **kwargs) + if result is not None: + return result[0] + + def find_line_index(self, *args, **kwargs): + """Return just the index from self.find_line_and_index().""" + result = self.find_line_and_index(*args, **kwargs) + if result is not None: + return result[1] + + def find_line_and_index(self, lines, commands, parameters=None, start=0, + end=None, whole=True): + """Find the first line in lines that matches the given criteria. + + lines: The iterable of strings to search + commands: The command string (or iterable thereof) with which the + line must start. If given an iterable (e.g. list), the + line can match *any* given command. + parameters: The parameter string (or iterable thereof) that the line + must contain. Specifically, self.getValue() must return + a value. If gien an iterable, the line must contain + *all* of the given parameters. (Optional) + start: The index after which to search lines. (Optional) + end: The index before witch to seach lines. (Optional) + whole: If true, only match on whole commands. If false, match + any command prefix. E.g. with whole=True, G1 will match + only G1 commands. With whole=False, G1 would match G1 and + G10 commands, or even G1butterfly commands. :) E.g. To + find all T commands, use commands='T' and whole=False. + + Returns: The matching line string and its index in lines as a tuple, or + None if not match was found. + """ + if isinstance(commands, str): + commands = (commands,) + if isinstance(parameters, str): + parameters = (parameters,) + if end is None: + end = len(lines) + for i, line in enumerate(lines[start:end], start): + for command in commands: + # Commands must be standalone, or there must be a space before + # the first parameter. This distinguise between G1 and G10, + # for example. + if (line == command + or line.startswith(command + (' ' if whole else ''))): + if parameters is None: + return line, i + else: + values = (self.getValue(line, p) for p in parameters) + values = (v for v in values if v is not None) + # Consume iterators/generators and force into sequences + if len(tuple(values)) == len(tuple(parameters)): + return line, i + + def getValue(self, line, key, default = None): + """Replacement version of getValue that fixes a couple bugs. + + Specifically, it allows variable length keys and should support missing + leading zeros on values < 1mm (e.g. X.45). CuraEngine likes to emit + those sometimes now. :( + """ + key_pos = line.find(key) + if key_pos == -1 or (';' in line and key_pos > line.find(';')): + return default + sub_part = line[key_pos + len(key):] + m = re.search('^[0-9]*\.?[0-9]*', sub_part) + if m is None: + return default + try: + return float(m.group(0)) + except: + return default \ No newline at end of file diff --git a/resources/definitions/Mark2_for_Ultimaker2.def.json b/resources/definitions/Mark2_for_Ultimaker2.def.json new file mode 100644 index 0000000000..f5f0af5de5 --- /dev/null +++ b/resources/definitions/Mark2_for_Ultimaker2.def.json @@ -0,0 +1,241 @@ +{ + "id": "Mark2_for_Ultimaker2", + "version": 2, + "name": "Mark2 for Ultimaker2", + "inherits": "ultimaker2_plus", + "metadata": { + "visible": true, + "author": "TheUltimakerCommunity", + "manufacturer": "Foehnsturm", + "category": "Other", + "has_variants": true, + "has_materials": true, + "has_machine_materials": false, + "has_machine_quality": false, + "has_variant_materials": false, + "weight": 2, + "file_formats": "text/x-gcode", + "icon": "icon_ultimaker.png", + "platform": "ultimaker2_platform.obj", + "platform_texture": "Mark2_for_Ultimaker2_backplate.png", + "machine_extruder_trains": + { + "0": "Mark2_extruder1", + "1": "Mark2_extruder2" + }, + "supported_actions": ["MachineSettingsAction", "UpgradeFirmware"] + }, + "overrides": { + "machine_name": { "default_value": "Mark2_for_Ultimaker2" }, + "machine_width": { + "default_value": 223 + }, + "machine_depth": { + "default_value": 223 + }, + "machine_height": { + "default_value": 203 + }, + "gantry_height": { + "default_value": 52 + }, + "machine_center_is_zero": { + "default_value": false + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, + "machine_nozzle_heat_up_speed": { + "default_value": 3.5 + }, + "machine_nozzle_cool_down_speed": { + "default_value": 1.5 + }, + "machine_min_cool_heat_time_window": + { + "default_value": 15.0 + }, + "machine_show_variants": { + "default_value": true + }, + "machine_nozzle_head_distance": { + "default_value": 5 + }, + "machine_nozzle_expansion_angle": { + "default_value": 45 + }, + "machine_heat_zone_length": { + "default_value": 20 + }, + "machine_heated_bed": { + "default_value": true + }, + "speed_infill": { + "value": "speed_print" + }, + "speed_wall_x": { + "value": "speed_wall" + }, + "layer_height_0": { + "value": "round(machine_nozzle_size / 1.5, 2)" + }, + "line_width": { + "value": "round(machine_nozzle_size * 0.875, 2)" + }, + "speed_layer_0": { + "default_value": 20 + }, + "speed_support": { + "value": "speed_wall_0" + }, + "machine_max_feedrate_x": { + "default_value": 250 + }, + "machine_max_feedrate_y": { + "default_value": 250 + }, + "machine_max_feedrate_z": { + "default_value": 40 + }, + "machine_max_feedrate_e": { + "default_value": 45 + }, + "machine_acceleration": { + "default_value": 3000 + }, + "retraction_amount": { + "default_value": 5.1 + }, + "retraction_speed": { + "default_value": 25 + }, + "switch_extruder_retraction_amount": { + "default_value": 0, + "value": "retraction_amount", + "enabled": false + }, + "switch_extruder_retraction_speeds": { + "default_value": 25, + "value": "retraction_speed", + "enabled": false + }, + "switch_extruder_retraction_speed": { + "default_value": 25, + "value": "retraction_retract_speed", + "enabled": false + }, + "switch_extruder_prime_speed": { + "default_value": 25, + "value": "retraction_prime_speed", + "enabled": false + }, + "machine_head_with_fans_polygon": + { + "default_value": [ + [ -44, 14 ], + [ -44, -34 ], + [ 64, 14 ], + [ 64, -34 ] + ] + }, + "machine_use_extruder_offset_to_offset_coords": { + "default_value": false + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_start_gcode" : { + "default_value": "", + "value": "\"\" if machine_gcode_flavor == \"UltiGCode\" else \"G21 ;metric values\\nG90 ;absolute positioning\\nM82 ;set extruder to absolute mode\\nM107 ;start with the fan off\\nM200 D0 T0 ;reset filament diameter\\nM200 D0 T1\\nG28 Z0; home all\\nG28 X0 Y0\\nG0 Z20 F2400 ;move the platform to 20mm\\nG92 E0\\nM190 S{material_bed_temperature_layer_0}\\nM109 T0 S{material_standby_temperature, 0}\\nM109 T1 S{material_print_temperature_layer_0, 1}\\nM104 T0 S{material_print_temperature_layer_0, 0}\\nT1 ; move to the 2th head\\nG0 Z20 F2400\\nG92 E-7.0 ;prime distance\\nG1 E0 F45 ;purge nozzle\\nG1 E-5.1 F1500 ; retract\\nG1 X90 Z0.01 F5000 ; move away from the prime poop\\nG1 X50 F9000\\nG0 Z20 F2400\\nT0 ; move to the first head\\nM104 T1 S{material_standby_temperature, 1}\\nG0 Z20 F2400\\nM104 T{initial_extruder_nr} S{material_print_temperature_layer_0, initial_extruder_nr}\\nG92 E-7.0\\nG1 E0 F45 ;purge nozzle\\nG1 X60 Z0.01 F5000 ; move away from the prime poop\\nG1 X20 F9000\\nM400 ;finish all moves\\nG92 E0\\n;end of startup sequence\\n\"" + }, + "machine_end_gcode" : { + "default_value": "", + "value": "\"\" if machine_gcode_flavor == \"UltiGCode\" else \"G90 ;absolute positioning\\nM104 S0 T0 ;extruder heater off\\nM104 S0 T1\\nM140 S0 ;turn off bed\\nT0 ; move to the first head\\nM107 ;fan off\"" + }, + "machine_extruder_count": { + "default_value": 2 + }, + "acceleration_enabled": + { + "default_value": true + }, + "acceleration_print": + { + "default_value": 2000, + "value": "2000" + }, + "acceleration_travel": + { + "default_value": 3000, + "value": "acceleration_print if magic_spiralize else 3000" + }, + "acceleration_layer_0": { "value": "acceleration_topbottom" }, + "acceleration_prime_tower": { "value": "math.ceil(acceleration_print * 2000 / 4000)" }, + "acceleration_support": { "value": "math.ceil(acceleration_print * 2000 / 4000)" }, + "acceleration_support_interface": { "value": "acceleration_topbottom" }, + "acceleration_topbottom": { "value": "math.ceil(acceleration_print * 500 / 4000)" }, + "acceleration_wall": { "value": "math.ceil(acceleration_print * 1000 / 4000)" }, + "acceleration_wall_0": { "value": "math.ceil(acceleration_wall * 500 / 1000)" }, + "jerk_enabled": + { + "default_value": true + }, + "jerk_print": + { + "default_value": 12 + }, + "jerk_travel": + { + "default_value": 20, + "value": "jerk_print if magic_spiralize else 20" + }, + "jerk_layer_0": { "value": "jerk_topbottom" }, + "jerk_prime_tower": { "value": "10 if jerk_print < 16 else math.ceil(jerk_print * 15 / 25)" }, + "jerk_support": { "value": "10 if jerk_print < 16 else math.ceil(jerk_print * 15 / 25)" }, + "jerk_support_interface": { "value": "jerk_topbottom" }, + "jerk_topbottom": { "value": "10 if jerk_print < 25 else math.ceil(jerk_print * 10 / 25)" }, + "jerk_wall": { "value": "10 if jerk_print < 16 else math.ceil(jerk_print * 15 / 25)" }, + "jerk_wall_0": { "value": "10 if jerk_wall < 16 else math.ceil(jerk_wall * 6 / 10)" }, + "jerk_travel_layer_0": { "value": "math.ceil(jerk_layer_0 * jerk_travel / jerk_print)" }, + "extruder_prime_pos_abs": { "default_value": false }, + "machine_extruder_start_pos_abs": { "default_value": false }, + "machine_extruder_start_pos_x": { "value": 0.0 }, + "machine_extruder_start_pos_y": { "value": 0.0 }, + "machine_extruder_end_pos_abs": { "default_value": false }, + "machine_extruder_end_pos_x": { "value": 0.0 }, + "machine_extruder_end_pos_y": { "value": 0.0 }, + "extruder_prime_pos_x": { "default_value": 0.0, "enabled": false }, + "extruder_prime_pos_y": { "default_value": 0.0, "enabled": false }, + "extruder_prime_pos_z": { "default_value": 0.0, "enabled": false }, + "start_layers_at_same_position": + { + "default_value": false, + "enabled": false, + "value": false + }, + "layer_start_x": + { + "default_value": 105.0, + "enabled": false + }, + "layer_start_y": + { + "default_value": 27.0, + "enabled": false + }, + "prime_tower_position_x": { + "default_value": 185 + }, + "prime_tower_position_y": { + "default_value": 160 + }, + "machine_disallowed_areas": { + "default_value": [ + [[-115, 112.5], [ -10, 112.5], [ -10, 72.5], [-115, 72.5]], + [[ 115, 112.5], [ 115, 72.5], [ 15, 72.5], [ 15, 112.5]], + [[-115, -112.5], [-115, -87.5], [ 115, -87.5], [ 115, -112.5]], + [[-115, 72.5], [-97, 72.5], [-97, -112.5], [-115, -112.5]] + ] + } + } +} diff --git a/resources/extruders/Mark2_extruder1.def.json b/resources/extruders/Mark2_extruder1.def.json new file mode 100644 index 0000000000..915c331083 --- /dev/null +++ b/resources/extruders/Mark2_extruder1.def.json @@ -0,0 +1,19 @@ +{ + "id": "Mark2_extruder1", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "Mark2_for_Ultimaker2", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0.0 }, + "machine_nozzle_offset_y": { "default_value": 0.0 } + } +} diff --git a/resources/extruders/Mark2_extruder2.def.json b/resources/extruders/Mark2_extruder2.def.json new file mode 100644 index 0000000000..2c05a09391 --- /dev/null +++ b/resources/extruders/Mark2_extruder2.def.json @@ -0,0 +1,19 @@ +{ + "id": "Mark2_extruder2", + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "Mark2_for_Ultimaker2", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0.0 }, + "machine_nozzle_offset_y": { "default_value": 0.0 } + } +} diff --git a/resources/images/Mark2_for_Ultimaker2_backplate.png b/resources/images/Mark2_for_Ultimaker2_backplate.png new file mode 100644 index 0000000000000000000000000000000000000000..c1958c730083cd91927d3f999a4bb05e066c85cb GIT binary patch literal 13655 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 Date: Mon, 1 Apr 2019 20:22:30 +0200 Subject: [PATCH 2/3] remove post processing script --- .../scripts/Mark2Tweaks.py | 312 ------------------ 1 file changed, 312 deletions(-) delete mode 100644 plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py diff --git a/plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py b/plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py deleted file mode 100644 index bc9168e931..0000000000 --- a/plugins/PostProcessingPlugin/scripts/Mark2Tweaks.py +++ /dev/null @@ -1,312 +0,0 @@ -## -## Mark2Plugin - Mark2Tweaks: Cura PostProcessingPlugin script for the Mark 2. -## Copyright (C) 2016,2017 Krys Lawrence -## -## This program is free software: you can redistribute it and/or modify -## it under the terms of the GNU Affero General Public License as -## published by the Free Software Foundation, either version 3 of the -## License, or (at your option) any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Affero General Public License for more details. -## -## You should have received a copy of the GNU Affero General Public License -## along with this program. If not, see . -## - -"""Add-on script for nallath's PostProcessingPlugin to help the Mark 2. - -See https://github.com/nallath/PostProcessingPlugin for details about the -plugin. - -Put this script in PostProcessingPlugin's scripts folder. -""" - - -import re -import traceback -import contextlib -from UM.Logger import Logger -from ..Script import Script - - -"""Convenience alias for UM.Logger.Logger.log.""" -log = Logger.log - - -@contextlib.contextmanager -def exception_handler(layer_num, log_not_raise=False): - """Either raise or just log the last exception.""" - try: - yield - except: - if log_not_raise: - layer_log(layer_num, 'e', ''.join(traceback.format_exc())) - else: - layer_log(layer_num, 'e', 'Exception! Traceback follows.') - raise - - -def layer_log(layer_num, message_type, message): - """Log a message prefixed with the curent layer number.""" - log(message_type, 'Layer {:.0f}: {}'.format(layer_num, message)) - - -class Mark2Tweaks(Script): - """Optimize the G-code output for the Mark 2.""" - - def getSettingDataString(self): - """Return script identification and GUI options.""" - # Note: The "version" key is not this script's version, but the API - # version of the PostProcessingPlugin. - return '''\ - { - "name": "Mark 2 Tweaks", - "key": "Mark2Tweaks", - "metadata": {}, - "version": 2, - "settings": { - "remove_hack": { - "label": "Clean Up Cura Workaround", - "description": - "The Mark 2 settings include a workaround to a Cura limitation. This tweak cleans up after it.", - "type": "bool", - "default_value": true - }, - "remove_superfluous": { - "label": "Remove Extra Movements", - "description": - "Remove superfluous movements after each tool change. This can improve print quality by preventing materials from incorrectly touching immediately after a tool change.", - "type": "bool", - "default_value": true - }, - "ignore_errors": { - "label": "Ignore Errors", - "description": - "If any errors occur while performing the tweaks, skip them keep going", - "type": "bool", - "default_value": true - } - } - }''' - - def execute(self, data): - """Process all G-code and apply selected tweaks.""" - log('d', '*** MARK 2 TWEAKS START ***') - remove_hack = self.getSettingValueByKey('remove_hack') - remove_superfluous = self.getSettingValueByKey('remove_superfluous') - ignore_errors = self.getSettingValueByKey('ignore_errors') - log('d', 'Remove Hack: {}'.format(remove_hack)) - log('d', 'Remove Superfluous: {}'.format(remove_superfluous)) - log('d', 'Ignore Errors: {}'.format(ignore_errors)) - for layer_idx, layer in enumerate(data): - lines = layer.split('\n') - layer_num = self.find_layer_num(lines) - if layer_num is None: - continue - # Copy of lines so lines can be deleted or inserted in loop - for line_idx, line in enumerate(lines[:]): - if not line in ('T0', 'T1'): - continue - if remove_hack: - with exception_handler(layer_num, ignore_errors): - self.remove_hack(layer_num, lines, line_idx) - if remove_superfluous: - with exception_handler(layer_num, ignore_errors): - self.remove_superfluous(layer_num, lines, line_idx) - data[layer_idx] = '\n'.join(lines) - log('d', '*** MARK 2 TWEAKS END ***') - return data - - def find_layer_num(self, lines): - """Return the current layer number as a float.""" - result = self.find_line(lines, ';LAYER:', whole=False) - if result is not None: - return self.getValue(result, ";LAYER:") - - def remove_hack(self, layer_num, lines, t_idx): - """Remove TinkerGnome's Cura print area workaround line. - - If there is a G0 between T and G10/M104, remove it. - There should only be one. - TinkerGnome says adding it was a hack/workaround so we can kill it. - """ - end_idx = self.find_g10_or_m104(layer_num, lines, t_idx) - hack = self.find_line_and_index(lines, 'G0', ('X', 'Y', 'Z'), t_idx, - end_idx) - if hack is None: - return - hack_line, hack_idx = hack - if (self.getValue(hack_line, 'Z') == 14 - and self.getValue(hack_line, 'Y') == 35): - layer_log(layer_num, 'd', 'Striping Cura print area hack.') - del lines[hack_idx] - - def remove_superfluous(self, layer_num, lines, t_idx): - """Collapse any post tool change movents into a single movement. - - Any non-extrusion movements after tool change should be collapsed into - a single line. Keep only the last G0/G1 but add the F and Z of the - first G0/G1. But only collapse if there is more than one line. - """ - start_idx = self.find_g10_or_m104(layer_num, lines, t_idx) - end_idx = self.find_line_index(lines, ('G0', 'G1'), 'E', start_idx) - assert end_idx is not None, \ - 'Cannot find extruding G0/G1 line after tool change.' - - first_g = self.find_line_and_index(lines, ('G0', 'G1'), None, - start_idx, end_idx) - assert first_g is not None, \ - 'Sanity Check: Could not find a G0/G1 line before extrusion and ' \ - 'after tool change.' - first_g_line, first_g_idx = first_g - assert first_g_idx < end_idx, \ - 'Sanity Check: First G0/G1 is >= to first extrusion.' - - f_value = self.getValue(first_g_line, 'F') - z_value = self.getValue(first_g_line, 'Z') - assert z_value is not None, \ - 'Sanity Check: Z value not found in first G0/G1 line.' - - self.delete_all_g0_or_g1_except_last(layer_num, lines, first_g_idx, - 'Collapsing post tool change movements.') - assert self.is_g0_or_g1(lines[first_g_idx]), \ - 'Sanity Check: Missing G0/G1 after collapse.' - assert not self.is_g0_or_g1(lines[first_g_idx+1]), \ - 'Sanity Check: More than one G0/G1 after collapse.' - - self.add_f_and_z_values(layer_num, lines, first_g_idx, z_value, - f_value) - assert self.getValue(lines[first_g_idx], 'Z') is not None, \ - 'Sanity Check: Missing required Z value.' - - def find_g10_or_m104(self, layer_num, lines, t_idx): - """Find the next G10 or M104 line. - - G10 is for UltiGCode-style G-code. - M104 is for RepRap-style G-code. - """ - idx = self.find_line_index(lines, 'G10', start=t_idx) - if idx is None: # Assume RepRap style G-code - idx = self.find_line_index(lines, 'M104', start=t_idx) - assert idx is not None, \ - 'Cannot find G10/M104 after tool change.' - assert t_idx < idx < t_idx + 10, \ - 'Sanity Check: G10/M104 too far from T' - return idx - - def delete_all_g0_or_g1_except_last(self, layer_num, lines, first_g_idx, - log_msg): - """Delete all G0/G1 lines, except the last one. - - As long as there is more than one G line, delete the first. - Subsequent G line indices move up by one == first_g_idx. - This works only if lines are deleted and not just replaced. - If only one G, never run. Last G is not deleted. - - Also log only once if one or more deletes occurs. - """ - has_logged = False - while self.is_g0_or_g1(lines[first_g_idx+1]): - if not has_logged: - # Never log on single line. Only log once if multiple lines. - layer_log(layer_num, 'd', log_msg) - has_logged = True - del lines[first_g_idx] - - def is_g0_or_g1(self, line): - """Return true is line is a G0 or G1 command.""" - return line.startswith('G0 ') or line.startswith('G1 ') - - def add_f_and_z_values(self, layer_num, lines, g_idx, z_value, - f_value=None): - """Add Z and F values to the indicated G0/G1 line. - - f_value is optional. - Existing Z and F values will not be replaced. - """ - line = lines[g_idx] - fields = line.split(' ') - if f_value is not None and self.getValue(line, 'F') is None: - fields.insert(1, 'F{}'.format(f_value)) - if self.getValue(line, 'Z') is None: - fields.append('Z{}'.format(z_value)) - lines[g_idx] = ' '.join(fields) - - def find_line(self, *args, **kwargs): - """Return just the line from self.find_line_and_index().""" - result = self.find_line_and_index(*args, **kwargs) - if result is not None: - return result[0] - - def find_line_index(self, *args, **kwargs): - """Return just the index from self.find_line_and_index().""" - result = self.find_line_and_index(*args, **kwargs) - if result is not None: - return result[1] - - def find_line_and_index(self, lines, commands, parameters=None, start=0, - end=None, whole=True): - """Find the first line in lines that matches the given criteria. - - lines: The iterable of strings to search - commands: The command string (or iterable thereof) with which the - line must start. If given an iterable (e.g. list), the - line can match *any* given command. - parameters: The parameter string (or iterable thereof) that the line - must contain. Specifically, self.getValue() must return - a value. If gien an iterable, the line must contain - *all* of the given parameters. (Optional) - start: The index after which to search lines. (Optional) - end: The index before witch to seach lines. (Optional) - whole: If true, only match on whole commands. If false, match - any command prefix. E.g. with whole=True, G1 will match - only G1 commands. With whole=False, G1 would match G1 and - G10 commands, or even G1butterfly commands. :) E.g. To - find all T commands, use commands='T' and whole=False. - - Returns: The matching line string and its index in lines as a tuple, or - None if not match was found. - """ - if isinstance(commands, str): - commands = (commands,) - if isinstance(parameters, str): - parameters = (parameters,) - if end is None: - end = len(lines) - for i, line in enumerate(lines[start:end], start): - for command in commands: - # Commands must be standalone, or there must be a space before - # the first parameter. This distinguise between G1 and G10, - # for example. - if (line == command - or line.startswith(command + (' ' if whole else ''))): - if parameters is None: - return line, i - else: - values = (self.getValue(line, p) for p in parameters) - values = (v for v in values if v is not None) - # Consume iterators/generators and force into sequences - if len(tuple(values)) == len(tuple(parameters)): - return line, i - - def getValue(self, line, key, default = None): - """Replacement version of getValue that fixes a couple bugs. - - Specifically, it allows variable length keys and should support missing - leading zeros on values < 1mm (e.g. X.45). CuraEngine likes to emit - those sometimes now. :( - """ - key_pos = line.find(key) - if key_pos == -1 or (';' in line and key_pos > line.find(';')): - return default - sub_part = line[key_pos + len(key):] - m = re.search('^[0-9]*\.?[0-9]*', sub_part) - if m is None: - return default - try: - return float(m.group(0)) - except: - return default \ No newline at end of file From ebfd33ba104063b78c772340379e4c7790d64291 Mon Sep 17 00:00:00 2001 From: TinkerGnome Date: Wed, 3 Apr 2019 21:01:04 +0200 Subject: [PATCH 3/3] remove machine_extruder positions from the definition file --- resources/definitions/Mark2_for_Ultimaker2.def.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/resources/definitions/Mark2_for_Ultimaker2.def.json b/resources/definitions/Mark2_for_Ultimaker2.def.json index f5f0af5de5..0379d3967c 100644 --- a/resources/definitions/Mark2_for_Ultimaker2.def.json +++ b/resources/definitions/Mark2_for_Ultimaker2.def.json @@ -198,12 +198,6 @@ "jerk_wall_0": { "value": "10 if jerk_wall < 16 else math.ceil(jerk_wall * 6 / 10)" }, "jerk_travel_layer_0": { "value": "math.ceil(jerk_layer_0 * jerk_travel / jerk_print)" }, "extruder_prime_pos_abs": { "default_value": false }, - "machine_extruder_start_pos_abs": { "default_value": false }, - "machine_extruder_start_pos_x": { "value": 0.0 }, - "machine_extruder_start_pos_y": { "value": 0.0 }, - "machine_extruder_end_pos_abs": { "default_value": false }, - "machine_extruder_end_pos_x": { "value": 0.0 }, - "machine_extruder_end_pos_y": { "value": 0.0 }, "extruder_prime_pos_x": { "default_value": 0.0, "enabled": false }, "extruder_prime_pos_y": { "default_value": 0.0, "enabled": false }, "extruder_prime_pos_z": { "default_value": 0.0, "enabled": false },