diff --git a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py index cdbb4a79ef..239463bca4 100644 --- a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py +++ b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py @@ -4,71 +4,86 @@ # It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher. # This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms -#Authors of the ChangeAtZ plugin / script: +# Authors of the ChangeAtZ plugin / script: # Written by Steven Morlock, smorloc@gmail.com # Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+ # Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below) # Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x # Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug. +# Modified by Wes Hanney, https://github.com/novamxd, Retract Length + Speed, Clean up -##history / changelog: -##V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment) -##V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at, -## extruder three temperature disabled by "#Ex3" -##V3.1.1: Bugfix reset flow rate -##V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift -##V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser, -## added speed reset at the end of the print -##V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option, -## extruder three code removed, tweaking print speed, save call of Publisher class, -## uses previous value from other plugins also on UltiGCode -##V4.0.1: Bugfix for doubled G1 commands -##V4.0.2: uses Cura progress bar instead of its own -##V4.0.3: Bugfix for cool head lift (contributed by luisonoff) -##V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin -##V4.9.92: Modifications for Cura 15.10 -##V4.9.93: Minor bugfixes (input settings) / documentation -##V4.9.94: Bugfix Combobox-selection; remove logger -##V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x -##V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed' -##V5.1: API Changes included for use with Cura 2.2 +# history / changelog: +# V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment) +# V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at, +# extruder three temperature disabled by "#Ex3" +# V3.1.1: Bugfix reset flow rate +# V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift +# V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser, +# added speed reset at the end of the print +# V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option, +# extruder three code removed, tweaking print speed, save call of Publisher class, +# uses previous value from other plugins also on UltiGCode +# V4.0.1: Bugfix for doubled G1 commands +# V4.0.2: Uses Cura progress bar instead of its own +# V4.0.3: Bugfix for cool head lift (contributed by luisonoff) +# V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin +# V4.9.92: Modifications for Cura 15.10 +# V4.9.93: Minor bugfixes (input settings) / documentation +# V4.9.94: Bugfix Combobox-selection; remove logger +# V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x +# V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed' +# V5.1: API Changes included for use with Cura 2.2 +# V5.2.0: Wes Hanney. Added support for changing Retract Length and Speed. Removed layer spread option. Fixed issue of cumulative ChangeZ +# mods so they can now properly be stacked on top of each other. Applied code refactoring to clean up various coding styles. Added comments. +# Broke up functions for clarity. Split up class so it can be debugged outside of Cura. +# V5.2.1: Wes Hanney. Added support for firmware based retractions. Fixed issue of properly restoring previous values in single layer option. +# Added support for outputting changes to LCD (untested). Added type hints to most functions and variables. Added more comments. Created GCodeCommand +# class for better detection of G1 vs G10 or G11 commands, and accessing arguments. Moved most GCode methods to GCodeCommand class. Improved wording +# of Single Layer vs Keep Layer to better reflect what was happening. -## Uses - -## M220 S - set speed factor override percentage -## M221 S - set flow factor override percentage -## M221 S T<0-#toolheads> - set flow factor override percentage for single extruder -## M104 S T<0-#toolheads> - set extruder to target temperature -## M140 S - set bed target temperature -## M106 S - set fan speed to target speed -## M605/606 to save and recall material settings on the UM2 +# Uses - +# M220 S - set speed factor override percentage +# M221 S - set flow factor override percentage +# M221 S T<0-#toolheads> - set flow factor override percentage for single extruder +# M104 S T<0-#toolheads> - set extruder to target temperature +# M140 S - set bed target temperature +# M106 S - set fan speed to target speed +# M207 S F - set the retract length or feed rate +# M117 - output the current changes +from typing import List, Optional, Dict from ..Script import Script -#from UM.Logger import Logger import re + +# this was broken up into a separate class so the main ChangeZ script could be debugged outside of Cura class ChangeAtZ(Script): - version = "5.1.1" - def __init__(self): - super().__init__() + version = "5.2.1" def getSettingDataString(self): return """{ - "name":"ChangeAtZ """ + self.version + """ (Experimental)", - "key":"ChangeAtZ", + "name": "ChangeAtZ """ + self.version + """(Experimental)", + "key": "ChangeAtZ", "metadata": {}, "version": 2, - "settings": - { - "a_trigger": - { + "settings": { + "caz_enabled": { + "label": "Enabled", + "description": "Allows adding multiple ChangeZ mods and disabling them as needed.", + "type": "bool", + "default_value": true + }, + "a_trigger": { "label": "Trigger", "description": "Trigger at height or at layer no.", "type": "enum", - "options": {"height":"Height","layer_no":"Layer No."}, + "options": { + "height": "Height", + "layer_no": "Layer No." + }, "default_value": "height" }, - "b_targetZ": - { + "b_targetZ": { "label": "Change Height", "description": "Z height to change at", "unit": "mm", @@ -79,8 +94,7 @@ class ChangeAtZ(Script): "maximum_value_warning": "230", "enabled": "a_trigger == 'height'" }, - "b_targetL": - { + "b_targetL": { "label": "Change Layer", "description": "Layer no. to change at", "unit": "", @@ -90,34 +104,29 @@ class ChangeAtZ(Script): "minimum_value_warning": "-1", "enabled": "a_trigger == 'layer_no'" }, - "c_behavior": - { - "label": "Behavior", - "description": "Select behavior: Change value and keep it for the rest, Change value for single layer only", + "c_behavior": { + "label": "Apply To", + "description": "Target Layer + Subsequent Layers is good for testing changes between ranges of layers, ex: Layer 0 to 10 or 0mm to 5mm. Single layer is good for testing changes at a single layer, ex: at Layer 10 or 5mm only.", "type": "enum", - "options": {"keep_value":"Keep value","single_layer":"Single Layer"}, + "options": { + "keep_value": "Target Layer + Subsequent Layers", + "single_layer": "Target Layer Only" + }, "default_value": "keep_value" }, - "d_twLayers": - { - "label": "Layer Spread", - "description": "The change will be gradual over this many layers. Enter 1 to make the change immediate.", - "unit": "", - "type": "int", - "default_value": 1, - "minimum_value": "1", - "maximum_value_warning": "50", - "enabled": "c_behavior == 'keep_value'" - }, - "e1_Change_speed": - { + "caz_output_to_display": { + "label": "Output to Display", + "description": "Displays the current changes to the LCD", + "type": "bool", + "default_value": false + }, + "e1_Change_speed": { "label": "Change Speed", "description": "Select if total speed (print and travel) has to be changed", "type": "bool", "default_value": false }, - "e2_speed": - { + "e2_speed": { "label": "Speed", "description": "New total speed (print and travel)", "unit": "%", @@ -128,15 +137,13 @@ class ChangeAtZ(Script): "maximum_value_warning": "200", "enabled": "e1_Change_speed" }, - "f1_Change_printspeed": - { + "f1_Change_printspeed": { "label": "Change Print Speed", "description": "Select if print speed has to be changed", "type": "bool", "default_value": false }, - "f2_printspeed": - { + "f2_printspeed": { "label": "Print Speed", "description": "New print speed", "unit": "%", @@ -147,15 +154,13 @@ class ChangeAtZ(Script): "maximum_value_warning": "200", "enabled": "f1_Change_printspeed" }, - "g1_Change_flowrate": - { + "g1_Change_flowrate": { "label": "Change Flow Rate", "description": "Select if flow rate has to be changed", "type": "bool", "default_value": false }, - "g2_flowrate": - { + "g2_flowrate": { "label": "Flow Rate", "description": "New Flow rate", "unit": "%", @@ -166,15 +171,13 @@ class ChangeAtZ(Script): "maximum_value_warning": "200", "enabled": "g1_Change_flowrate" }, - "g3_Change_flowrateOne": - { + "g3_Change_flowrateOne": { "label": "Change Flow Rate 1", "description": "Select if first extruder flow rate has to be changed", "type": "bool", "default_value": false }, - "g4_flowrateOne": - { + "g4_flowrateOne": { "label": "Flow Rate One", "description": "New Flow rate Extruder 1", "unit": "%", @@ -185,15 +188,13 @@ class ChangeAtZ(Script): "maximum_value_warning": "200", "enabled": "g3_Change_flowrateOne" }, - "g5_Change_flowrateTwo": - { + "g5_Change_flowrateTwo": { "label": "Change Flow Rate 2", "description": "Select if second extruder flow rate has to be changed", "type": "bool", "default_value": false }, - "g6_flowrateTwo": - { + "g6_flowrateTwo": { "label": "Flow Rate two", "description": "New Flow rate Extruder 2", "unit": "%", @@ -204,15 +205,13 @@ class ChangeAtZ(Script): "maximum_value_warning": "200", "enabled": "g5_Change_flowrateTwo" }, - "h1_Change_bedTemp": - { + "h1_Change_bedTemp": { "label": "Change Bed Temp", "description": "Select if Bed Temperature has to be changed", "type": "bool", "default_value": false }, - "h2_bedTemp": - { + "h2_bedTemp": { "label": "Bed Temp", "description": "New Bed Temperature", "unit": "C", @@ -223,15 +222,13 @@ class ChangeAtZ(Script): "maximum_value_warning": "120", "enabled": "h1_Change_bedTemp" }, - "i1_Change_extruderOne": - { + "i1_Change_extruderOne": { "label": "Change Extruder 1 Temp", "description": "Select if First Extruder Temperature has to be changed", "type": "bool", "default_value": false }, - "i2_extruderOne": - { + "i2_extruderOne": { "label": "Extruder 1 Temp", "description": "New First Extruder Temperature", "unit": "C", @@ -242,15 +239,13 @@ class ChangeAtZ(Script): "maximum_value_warning": "250", "enabled": "i1_Change_extruderOne" }, - "i3_Change_extruderTwo": - { + "i3_Change_extruderTwo": { "label": "Change Extruder 2 Temp", "description": "Select if Second Extruder Temperature has to be changed", "type": "bool", "default_value": false }, - "i4_extruderTwo": - { + "i4_extruderTwo": { "label": "Extruder 2 Temp", "description": "New Second Extruder Temperature", "unit": "C", @@ -261,239 +256,1266 @@ class ChangeAtZ(Script): "maximum_value_warning": "250", "enabled": "i3_Change_extruderTwo" }, - "j1_Change_fanSpeed": - { + "j1_Change_fanSpeed": { "label": "Change Fan Speed", "description": "Select if Fan Speed has to be changed", "type": "bool", "default_value": false }, - "j2_fanSpeed": - { + "j2_fanSpeed": { "label": "Fan Speed", - "description": "New Fan Speed (0-255)", - "unit": "PWM", + "description": "New Fan Speed (0-100)", + "unit": "%", "type": "int", - "default_value": 255, + "default_value": 100, "minimum_value": "0", - "minimum_value_warning": "15", - "maximum_value_warning": "255", + "minimum_value_warning": "0", + "maximum_value_warning": "100", "enabled": "j1_Change_fanSpeed" - } + }, + "caz_change_retract": { + "label": "Change Retraction", + "description": "Indicates you would like to modify retraction properties.", + "type": "bool", + "default_value": false + }, + "caz_retractstyle": { + "label": "Retract Style", + "description": "Specify if you're using firmware retraction or linear move based retractions. Check your printer settings to see which you're using.", + "type": "enum", + "options": { + "linear": "Linear Move", + "firmware": "Firmware" + }, + "default_value": "linear", + "enabled": "caz_change_retract" + }, + "caz_change_retractfeedrate": { + "label": "Change Retract Feed Rate", + "description": "Changes the retraction feed rate during print", + "type": "bool", + "default_value": false, + "enabled": "caz_change_retract" + }, + "caz_retractfeedrate": { + "label": "Retract Feed Rate", + "description": "New Retract Feed Rate (mm/s)", + "unit": "mm/s", + "type": "float", + "default_value": 40, + "minimum_value": "0", + "minimum_value_warning": "0", + "maximum_value_warning": "100", + "enabled": "caz_change_retractfeedrate" + }, + "caz_change_retractlength": { + "label": "Change Retract Length", + "description": "Changes the retraction length during print", + "type": "bool", + "default_value": false, + "enabled": "caz_change_retract" + }, + "caz_retractlength": { + "label": "Retract Length", + "description": "New Retract Length (mm)", + "unit": "mm", + "type": "float", + "default_value": 6, + "minimum_value": "0", + "minimum_value_warning": "0", + "maximum_value_warning": "20", + "enabled": "caz_change_retractlength" + } } }""" - def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature - if not key in line or (";" in line and line.find(key) > line.find(";") and - not ";ChangeAtZ" in key and not ";LAYER:" in key): - return default - subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1 - if ";ChangeAtZ" in key: - m = re.search("^[0-4]", subPart) - elif ";LAYER:" in key: - m = re.search("^[+-]?[0-9]*", subPart) - else: - #the minus at the beginning allows for negative values, e.g. for delta printers - m = re.search("^[-]?[0-9]*\.?[0-9]*", subPart) - if m == None: - return default - try: - return float(m.group(0)) - except: - return default + def __init__(self): + super().__init__() def execute(self, data): - #Check which changes should apply - ChangeProp = {"speed": self.getSettingValueByKey("e1_Change_speed"), - "flowrate": self.getSettingValueByKey("g1_Change_flowrate"), - "flowrateOne": self.getSettingValueByKey("g3_Change_flowrateOne"), - "flowrateTwo": self.getSettingValueByKey("g5_Change_flowrateTwo"), - "bedTemp": self.getSettingValueByKey("h1_Change_bedTemp"), - "extruderOne": self.getSettingValueByKey("i1_Change_extruderOne"), - "extruderTwo": self.getSettingValueByKey("i3_Change_extruderTwo"), - "fanSpeed": self.getSettingValueByKey("j1_Change_fanSpeed")} - ChangePrintSpeed = self.getSettingValueByKey("f1_Change_printspeed") - ChangeStrings = {"speed": "M220 S%f\n", - "flowrate": "M221 S%f\n", - "flowrateOne": "M221 T0 S%f\n", - "flowrateTwo": "M221 T1 S%f\n", - "bedTemp": "M140 S%f\n", - "extruderOne": "M104 S%f T0\n", - "extruderTwo": "M104 S%f T1\n", - "fanSpeed": "M106 S%d\n"} - target_values = {"speed": self.getSettingValueByKey("e2_speed"), - "printspeed": self.getSettingValueByKey("f2_printspeed"), - "flowrate": self.getSettingValueByKey("g2_flowrate"), - "flowrateOne": self.getSettingValueByKey("g4_flowrateOne"), - "flowrateTwo": self.getSettingValueByKey("g6_flowrateTwo"), - "bedTemp": self.getSettingValueByKey("h2_bedTemp"), - "extruderOne": self.getSettingValueByKey("i2_extruderOne"), - "extruderTwo": self.getSettingValueByKey("i4_extruderTwo"), - "fanSpeed": self.getSettingValueByKey("j2_fanSpeed")} - old = {"speed": -1, "flowrate": 100, "flowrateOne": -1, "flowrateTwo": -1, "platformTemp": -1, "extruderOne": -1, - "extruderTwo": -1, "bedTemp": -1, "fanSpeed": -1, "state": -1} - twLayers = self.getSettingValueByKey("d_twLayers") - if self.getSettingValueByKey("c_behavior") == "single_layer": - behavior = 1 - else: - behavior = 0 - try: - twLayers = max(int(twLayers),1) #for the case someone entered something as "funny" as -1 - except: - twLayers = 1 - pres_ext = 0 - done_layers = 0 - z = 0 - x = None - y = None - layer = -100000 #layer no. may be negative (raft) but never that low - # state 0: deactivated, state 1: activated, state 2: active, but below z, - # state 3: active and partially executed (multi layer), state 4: active and passed z - state = 1 - # IsUM2: Used for reset of values (ok for Marlin/Sprinter), - # has to be set to 1 for UltiGCode (work-around for missing default values) - IsUM2 = False - oldValueUnknown = False - TWinstances = 0 - if self.getSettingValueByKey("a_trigger") == "layer_no": - targetL_i = int(self.getSettingValueByKey("b_targetL")) - targetZ = 100000 + caz_instance = ChangeAtZProcessor() + + caz_instance.TargetValues = {} + + # copy over our settings to our change z class + self.setIntSettingIfEnabled(caz_instance, "e1_Change_speed", "speed", "e2_speed") + self.setIntSettingIfEnabled(caz_instance, "f1_Change_printspeed", "printspeed", "f2_printspeed") + self.setIntSettingIfEnabled(caz_instance, "g1_Change_flowrate", "flowrate", "g2_flowrate") + self.setIntSettingIfEnabled(caz_instance, "g3_Change_flowrateOne", "flowrateOne", "g4_flowrateOne") + self.setIntSettingIfEnabled(caz_instance, "g5_Change_flowrateTwo", "flowrateTwo", "g6_flowrateTwo") + self.setFloatSettingIfEnabled(caz_instance, "h1_Change_bedTemp", "bedTemp", "h2_bedTemp") + self.setFloatSettingIfEnabled(caz_instance, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne") + self.setFloatSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo") + self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed") + self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractfeedrate", "retractfeedrate", "caz_retractfeedrate") + self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength") + + # is this mod enabled? + caz_instance.IsEnabled = self.getSettingValueByKey("caz_enabled") + + # are we emitting data to the LCD? + caz_instance.IsDisplayingChangesToLcd = self.getSettingValueByKey("caz_output_to_display") + + # are we doing linear move retractions? + caz_instance.IsLinearRetraction = self.getSettingValueByKey("caz_retractstyle") == "linear" + + # see if we're applying to a single layer or to all layers hence forth + caz_instance.IsApplyToSingleLayer = self.getSettingValueByKey("c_behavior") == "single_layer" + + # used for easy reference of layer or height targeting + caz_instance.IsTargetByLayer = self.getSettingValueByKey("a_trigger") == "layer_no" + + # change our target based on what we're targeting + caz_instance.TargetLayer = self.getIntSettingByKey("b_targetL", None) + caz_instance.TargetZ = self.getFloatSettingByKey("b_targetZ", None) + + # run our script + return caz_instance.execute(data) + + # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified + def setIntSettingIfEnabled(self, caz_instance, trigger, target, setting): + + # stop here if our trigger isn't enabled + if not self.getSettingValueByKey(trigger): + return + + # get our value from the settings + value = self.getIntSettingByKey(setting, None) + + # skip if there's no value or we can't interpret it + if value is None: + return + + # set our value in the target settings + caz_instance.TargetValues[target] = value + + # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified + def setFloatSettingIfEnabled(self, caz_instance, trigger, target, setting): + + # stop here if our trigger isn't enabled + if not self.getSettingValueByKey(trigger): + return + + # get our value from the settings + value = self.getFloatSettingByKey(setting, None) + + # skip if there's no value or we can't interpret it + if value is None: + return + + # set our value in the target settings + caz_instance.TargetValues[target] = value + + # Returns the given settings value as an integer or the default if it cannot parse it + def getIntSettingByKey(self, key, default): + + # change our target based on what we're targeting + try: + return int(self.getSettingValueByKey(key)) + except: + return default + + # Returns the given settings value as an integer or the default if it cannot parse it + def getFloatSettingByKey(self, key, default): + + # change our target based on what we're targeting + try: + return float(self.getSettingValueByKey(key)) + except: + return default + + +# This is a utility class for getting details of gcodes from a given line +class GCodeCommand: + + # The GCode command itself (ex: G10) + Command = None, + + # Contains any arguments passed to the command. The key is the argument name, the value is the value of the argument. + Arguments = {} + + # Contains the components of the command broken into pieces + Components = [] + + # Constructor. Sets up defaults + def __init__(self): + self.reset() + + # Gets a GCode Command from the given single line of GCode + @staticmethod + def getFromLine(line: str): + + # obviously if we don't have a command, we can't return anything + if line is None or len(line) == 0: + return None + + # we only support G or M commands + if line[0] != "G" and line[0] != "M": + return None + + # remove any comments + line = re.sub(r";.*$", "", line) + + # break into the individual components + command_pieces = line.strip().split(" ") + + # our return command details + command = GCodeCommand() + + # stop here if we don't even have something to interpret + if len(command_pieces) == 0: + return None + + # stores all the components of the command within the class for later + command.Components = command_pieces + + # set the actual command + command.Command = command_pieces[0] + + # stop here if we don't have any parameters + if len(command_pieces) == 1: + return None + + # return our indexed command + return command + + # Handy function for reading a linear move command + @staticmethod + def getLinearMoveCommand(line: str): + + # get our command from the line + linear_command = GCodeCommand.getFromLine(line) + + # if it's not a linear move, we don't care + if linear_command is None or (linear_command.Command != "G0" and linear_command.Command != "G1"): + return None + + # convert our values to floats (or defaults) + linear_command.Arguments["F"] = linear_command.getArgumentAsFloat("F", None) + linear_command.Arguments["X"] = linear_command.getArgumentAsFloat("X", None) + linear_command.Arguments["Y"] = linear_command.getArgumentAsFloat("Y", None) + linear_command.Arguments["Z"] = linear_command.getArgumentAsFloat("Z", None) + linear_command.Arguments["E"] = linear_command.getArgumentAsFloat("E", None) + + # return our new command + return linear_command + + # Gets the value of a parameter or returns the default if there is none + def getArgument(self, name: str, default: str = None) -> str: + + # parse our arguments (only happens once) + self.parseArguments() + + # if we don't have the parameter, return the default + if name not in self.Arguments: + return default + + # otherwise return the value + return self.Arguments[name] + + # Gets the value of a parameter as a float or returns the default + def getArgumentAsFloat(self, name: str, default: float = None) -> float: + + # try to parse as a float, otherwise return the default + try: + return float(self.getArgument(name, default)) + except: + return default + + # Gets the value of a parameter as an integer or returns the default + def getArgumentAsInt(self, name: str, default: int = None) -> int: + + # try to parse as a integer, otherwise return the default + try: + return int(self.getArgument(name, default)) + except: + return default + + # Allows retrieving values from the given GCODE line + @staticmethod + def getDirectArgument(line: str, key: str, default: str = None) -> str: + + if key not in line or (";" in line and line.find(key) > line.find(";") and ";ChangeAtZ" not in key and ";LAYER:" not in key): + return default + + # allows for string lengths larger than 1 + sub_part = line[line.find(key) + len(key):] + + if ";ChangeAtZ" in key: + m = re.search("^[0-4]", sub_part) + elif ";LAYER:" in key: + m = re.search("^[+-]?[0-9]*", sub_part) else: - targetL_i = -100000 - targetZ = self.getSettingValueByKey("b_targetZ") + # the minus at the beginning allows for negative values, e.g. for delta printers + m = re.search(r"^[-]?[0-9]*\.?[0-9]*", sub_part) + if m is None: + return default + + try: + return m.group(0) + except: + return default + + # Converts the command parameter to a int or returns the default + @staticmethod + def getDirectArgumentAsFloat(line: str, key: str, default: float = None) -> float: + + # get the value from the command + value = GCodeCommand.getDirectArgument(line, key, default) + + # stop here if it's the default + if value == default: + return value + + try: + return float(value) + except: + return default + + # Converts the command parameter to a int or returns the default + @staticmethod + def getDirectArgumentAsInt(line: str, key: str, default: int = None) -> int: + + # get the value from the command + value = GCodeCommand.getDirectArgument(line, key, default) + + # stop here if it's the default + if value == default: + return value + + try: + return int(value) + except: + return default + + # Parses the arguments of the command on demand, only once + def parseArguments(self): + + # stop here if we don't have any remaining components + if len(self.Components) <= 1: + return None + + # iterate and index all of our parameters, skip the first component as it's the command + for i in range(1, len(self.Components)): + + # get our component + component = self.Components[i] + + # get the first character of the parameter, which is the name + component_name = component[0] + + # get the value of the parameter (the rest of the string + component_value = None + + # get our value if we have one + if len(component) > 1: + component_value = component[1:] + + # index the argument + self.Arguments[component_name] = component_value + + # clear the components to we don't process again + self.Components = [] + + # Easy function for replacing any GCODE parameter variable in a given GCODE command + @staticmethod + def replaceDirectArgument(line: str, key: str, value: str) -> str: + return re.sub(r"(^|\s)" + key + r"[\d\.]+(\s|$)", r"\1" + key + str(value) + r"\2", line) + + # Resets the model back to defaults + def reset(self): + self.Command = None + self.Arguments = {} + + +# The primary ChangeAtZ class that does all the gcode editing. This was broken out into an +# independent class so it could be debugged using a standard IDE +class ChangeAtZProcessor: + + # Holds our current height + CurrentZ = None + + # Holds our current layer number + CurrentLayer = None + + # Indicates if we're only supposed to apply our settings to a single layer or multiple layers + IsApplyToSingleLayer = False + + # Indicates if this should emit the changes as they happen to the LCD + IsDisplayingChangesToLcd = False + + # Indicates that this mod is still enabled (or not) + IsEnabled = True + + # Indicates if we're processing inside the target layer or not + IsInsideTargetLayer = False + + # Indicates if we have restored the previous values from before we started our pass + IsLastValuesRestored = False + + # Indicates if the user has opted for linear move retractions or firmware retractions + IsLinearRetraction = True + + # Indicates if we're targetting by layer or height value + IsTargetByLayer = True + + # Indicates if we have injected our changed values for the given layer yet + IsTargetValuesInjected = False + + # Holds the last extrusion value, used with detecting when a retraction is made + LastE = None + + # An index of our gcodes which we're monitoring + LastValues = {} + + # The detected layer height from the gcode + LayerHeight = None + + # The target layer + TargetLayer = None + + # Holds the values the user has requested to change + TargetValues = {} + + # The target height in mm + TargetZ = None + + # Used to track if we've been inside our target layer yet + WasInsideTargetLayer = False + + # boots up the class with defaults + def __init__(self): + self.reset() + + # Modifies the given GCODE and injects the commands at the various targets + def execute(self, data): + + # short cut the whole thing if we're not enabled + if not self.IsEnabled: + return data + + # our layer cursor index = 0 + for active_layer in data: + + # will hold our updated gcode modified_gcode = "" + + # mark all the defaults for deletion + active_layer = self.markChangesForDeletion(active_layer) + + # break apart the layer into commands lines = active_layer.split("\n") + + # evaluate each command individually for line in lines: - if line.strip() == "": + + # trim or command + line = line.strip() + + # skip empty lines + if len(line) == 0: continue - if ";Generated with Cura_SteamEngine" in line: - TWinstances += 1 - modified_gcode += ";ChangeAtZ instances: %d\n" % TWinstances - if not ("M84" in line or "M25" in line or ("G1" in line and ChangePrintSpeed and (state==3 or state==4)) or - ";ChangeAtZ instances:" in line): - modified_gcode += line + "\n" - IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode! - if ";ChangeAtZ-state" in line: #checks for state change comment - state = self.getValue(line, ";ChangeAtZ-state", state) - if ";ChangeAtZ instances:" in line: - try: - tempTWi = int(line[20:]) - except: - tempTWi = TWinstances - TWinstances = tempTWi - if ";Small layer" in line: #checks for begin of Cool Head Lift - old["state"] = state - state = 0 - if ";LAYER:" in line: #new layer no. found - if state == 0: - state = old["state"] - layer = self.getValue(line, ";LAYER:", layer) - if targetL_i > -100000: #target selected by layer no. - if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for change on layer 0 - state = 2 - targetZ = z + 0.001 - if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd - pres_ext = self.getValue(line, "T", pres_ext) - if "M190" in line or "M140" in line and state < 3: #looking for bed temp, stops after target z is passed - old["bedTemp"] = self.getValue(line, "S", old["bedTemp"]) - if "M109" in line or "M104" in line and state < 3: #looking for extruder temp, stops after target z is passed - if self.getValue(line, "T", pres_ext) == 0: - old["extruderOne"] = self.getValue(line, "S", old["extruderOne"]) - elif self.getValue(line, "T", pres_ext) == 1: - old["extruderTwo"] = self.getValue(line, "S", old["extruderTwo"]) - if "M107" in line: #fan is stopped; is always updated in order not to miss switch off for next object - old["fanSpeed"] = 0 - if "M106" in line and state < 3: #looking for fan speed - old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"]) - if "M221" in line and state < 3: #looking for flow rate - tmp_extruder = self.getValue(line, "T", None) - if tmp_extruder == None: #check if extruder is specified - old["flowrate"] = self.getValue(line, "S", old["flowrate"]) - if old["flowrate"] == -1: - old["flowrate"] = 100.0 - elif tmp_extruder == 0: #first extruder - old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"]) - elif tmp_extruder == 1: #second extruder - old["flowrateTwo"] = self.getValue(line, "S", old["flowrateTwo"]) - if ("M84" in line or "M25" in line): - if state>0 and ChangeProp["speed"]: #"finish" commands for UM Original and UM2 - modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n" - modified_gcode += "M117 \n" - modified_gcode += line + "\n" - if "G1" in line or "G0" in line: - newZ = self.getValue(line, "Z", z) - x = self.getValue(line, "X", None) - y = self.getValue(line, "Y", None) - e = self.getValue(line, "E", None) - f = self.getValue(line, "F", None) - if 'G1' in line and ChangePrintSpeed and (state==3 or state==4): - # check for pure print movement in target range: - if x != None and y != None and f != None and e != None and newZ==z: - modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"), - self.getValue(line, "Y"), self.getValue(line, "E")) - else: #G1 command but not a print movement - modified_gcode += line + "\n" - # no changing on retraction hops which have no x and y coordinate: - if (newZ != z) and (x is not None) and (y is not None): - z = newZ - if z < targetZ and state == 1: - state = 2 - if z >= targetZ and state == 2: - state = 3 - done_layers = 0 - for key in ChangeProp: - if ChangeProp[key] and old[key]==-1: #old value is not known - oldValueUnknown = True - if oldValueUnknown: #the changing has to happen within one layer - twLayers = 1 - if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2) - modified_gcode += "M605 S%d;stores parameters before changing\n" % (TWinstances-1) - if behavior == 1: #single layer change only and then reset - twLayers = 1 - if ChangePrintSpeed and behavior == 0: - twLayers = done_layers + 1 - if state==3: - if twLayers-done_layers>0: #still layers to go? - if targetL_i > -100000: - modified_gcode += ";ChangeAtZ V%s: executed at Layer %d\n" % (self.version,layer) - modified_gcode += "M117 Printing... ch@L%4d\n" % layer - else: - modified_gcode += (";ChangeAtZ V%s: executed at %1.2f mm\n" % (self.version,z)) - modified_gcode += "M117 Printing... ch@%5.1f\n" % z - for key in ChangeProp: - if ChangeProp[key]: - modified_gcode += ChangeStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1)) - done_layers += 1 - else: - state = 4 - if behavior == 1: #reset values after one layer - if targetL_i > -100000: - modified_gcode += ";ChangeAtZ V%s: reset on Layer %d\n" % (self.version,layer) - else: - modified_gcode += ";ChangeAtZ V%s: reset at %1.2f mm\n" % (self.version,z) - if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting - modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1) - else: #executes on RepRap, UM2 with Ultigcode and Cura setting - for key in ChangeProp: - if ChangeProp[key]: - modified_gcode += ChangeStrings[key] % float(old[key]) - # re-activates the plugin if executed by pre-print G-command, resets settings: - if (z < targetZ or layer == 0) and state >= 3: #resets if below change level or at level 0 - state = 2 - done_layers = 0 - if targetL_i > -100000: - modified_gcode += ";ChangeAtZ V%s: reset below Layer %d\n" % (self.version, targetL_i) - else: - modified_gcode += ";ChangeAtZ V%s: reset below %1.2f mm\n" % (self.version, targetZ) - if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting - modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1) - else: #executes on RepRap, UM2 with Ultigcode and Cura setting - for key in ChangeProp: - if ChangeProp[key]: - modified_gcode += ChangeStrings[key] % float(old[key]) + + # update our layer number if applicable + self.processLayerNumber(line) + + # update our layer height if applicable + self.processLayerHeight(line) + + # check if we're at the target layer or not + self.processTargetLayer() + + # process any changes to the gcode + modified_gcode += self.processLine(line) + + # remove any marked defaults + modified_gcode = self.removeMarkedChanges(modified_gcode) + + # append our modified line data[index] = modified_gcode + index += 1 + + # return our modified gcode return data + + # Builds the restored layer settings based on the previous settings and returns the relevant GCODE lines + def getChangedLastValues(self) -> Dict[str, any]: + + # capture the values that we've changed + changed = {} + + # for each of our target values, get the value to restore + # no point in restoring values we haven't changed + for key in self.TargetValues: + + # skip target values we can't restore + if key not in self.LastValues: + continue + + # save into our changed + changed[key] = self.LastValues[key] + + # return our collection of changed values + return changed + + # Builds the relevant display feedback for each of the values + def getDisplayChangesFromValues(self, values: Dict[str, any]) -> str: + + # stop here if we're not outputting data + if not self.IsDisplayingChangesToLcd: + return "" + + # will hold all the default settings for the target layer + codes = [] + + # looking for wait for bed temp + if "bedTemp" in values: + codes.append("BedTemp: " + str(values["bedTemp"])) + + # set our extruder one temp (if specified) + if "extruderOne" in values: + codes.append("Extruder 1 Temp: " + str(values["extruderOne"])) + + # set our extruder two temp (if specified) + if "extruderTwo" in values: + codes.append("Extruder 2 Temp: " + str(values["extruderTwo"])) + + # set global flow rate + if "flowrate" in values: + codes.append("Extruder A Flow Rate: " + str(values["flowrate"])) + + # set extruder 0 flow rate + if "flowrateOne" in values: + codes.append("Extruder 1 Flow Rate: " + str(values["flowrateOne"])) + + # set second extruder flow rate + if "flowrateTwo" in values: + codes.append("Extruder 2 Flow Rate: " + str(values["flowrateTwo"])) + + # set our fan speed + if "fanSpeed" in values: + codes.append("Fan Speed: " + str(values["fanSpeed"])) + + # set feedrate percentage + if "speed" in values: + codes.append("Print Speed: " + str(values["speed"])) + + # set print rate percentage + if "printspeed" in values: + codes.append("Linear Print Speed: " + str(values["printspeed"])) + + # set retract rate + if "retractfeedrate" in values: + codes.append("Retract Feed Rate: " + str(values["retractfeedrate"])) + + # set retract length + if "retractlength" in values: + codes.append("Retract Length: " + str(values["retractlength"])) + + # stop here if there's nothing to output + if len(codes) == 0: + return "" + + # output our command to display the data + return "M117 " + ", ".join(codes) + "\n" + + # Converts the last values to something that can be output on the LCD + def getLastDisplayValues(self) -> str: + + # convert our last values to something we can output + return self.getDisplayChangesFromValues(self.getChangedLastValues()) + + # Converts the target values to something that can be output on the LCD + def getTargetDisplayValues(self) -> str: + + # convert our target values to something we can output + return self.getDisplayChangesFromValues(self.TargetValues) + + # Builds the the relevant GCODE lines from the given collection of values + def getCodeFromValues(self, values: Dict[str, any]) -> str: + + # will hold all the desired settings for the target layer + codes = self.getCodeLinesFromValues(values) + + # stop here if there are no values that require changing + if len(codes) == 0: + return "" + + # return our default block for this layer + return ";[CAZD:\n" + "\n".join(codes) + "\n;:CAZD]" + + # Builds the relevant GCODE lines from the given collection of values + def getCodeLinesFromValues(self, values: Dict[str, any]) -> List[str]: + + # will hold all the default settings for the target layer + codes = [] + + # looking for wait for bed temp + if "bedTemp" in values: + codes.append("M140 S" + str(values["bedTemp"])) + + # set our extruder one temp (if specified) + if "extruderOne" in values: + codes.append("M104 S" + str(values["extruderOne"]) + " T0") + + # set our extruder two temp (if specified) + if "extruderTwo" in values: + codes.append("M104 S" + str(values["extruderTwo"]) + " T1") + + # set our fan speed + if "fanSpeed" in values: + + # convert our fan speed percentage to PWM + fan_speed = int((float(values["fanSpeed"]) / 100.0) * 255) + + # add our fan speed to the defaults + codes.append("M106 S" + str(fan_speed)) + + # set global flow rate + if "flowrate" in values: + codes.append("M221 S" + str(values["flowrate"])) + + # set extruder 0 flow rate + if "flowrateOne" in values: + codes.append("M221 S" + str(values["flowrateOne"]) + " T0") + + # set second extruder flow rate + if "flowrateTwo" in values: + codes.append("M221 S" + str(values["flowrateTwo"]) + " T1") + + # set feedrate percentage + if "speed" in values: + codes.append("M220 S" + str(values["speed"]) + " T1") + + # set print rate percentage + if "printspeed" in values: + codes.append(";PRINTSPEED " + str(values["printspeed"]) + "") + + # set retract rate + if "retractfeedrate" in values: + + if self.IsLinearRetraction: + codes.append(";RETRACTFEEDRATE " + str(values["retractfeedrate"] * 60) + "") + else: + codes.append("M207 F" + str(values["retractfeedrate"] * 60) + "") + + # set retract length + if "retractlength" in values: + + if self.IsLinearRetraction: + codes.append(";RETRACTLENGTH " + str(values["retractlength"]) + "") + else: + codes.append("M207 S" + str(values["retractlength"]) + "") + + return codes + + # Builds the restored layer settings based on the previous settings and returns the relevant GCODE lines + def getLastValues(self) -> str: + + # build the gcode to restore our last values + return self.getCodeFromValues(self.getChangedLastValues()) + + # Builds the gcode to inject either the changed values we want or restore the previous values + def getInjectCode(self) -> str: + + # if we're now outside of our target layer and haven't restored our last values, do so now + if not self.IsInsideTargetLayer and self.WasInsideTargetLayer and not self.IsLastValuesRestored: + + # mark that we've injected the last values + self.IsLastValuesRestored = True + + # inject the defaults + return self.getLastValues() + "\n" + self.getLastDisplayValues() + + # if we're inside our target layer but haven't added our values yet, do so now + if self.IsInsideTargetLayer and not self.IsTargetValuesInjected: + + # mark that we've injected the target values + self.IsTargetValuesInjected = True + + # inject the defaults + return self.getTargetValues() + "\n" + self.getTargetDisplayValues() + + # nothing to do + return "" + + # Returns the unmodified GCODE line from previous ChangeZ edits + @staticmethod + def getOriginalLine(line: str) -> str: + + # get the change at z original (cazo) details + original_line = re.search(r"\[CAZO:(.*?):CAZO\]", line) + + # if we didn't get a hit, this is the original line + if original_line is None: + return line + + return original_line.group(1) + + # Builds the target layer settings based on the specified values and returns the relevant GCODE lines + def getTargetValues(self) -> str: + + # build the gcode to change our current values + return self.getCodeFromValues(self.TargetValues) + + # Determines if the current line is at or below the target required to start modifying + def isTargetLayerOrHeight(self) -> bool: + + # target selected by layer no. + if self.IsTargetByLayer: + + # if we don't have a current layer, we're not there yet + if self.CurrentLayer is None: + return False + + # if we're applying to a single layer, stop if our layer is not identical + if self.IsApplyToSingleLayer: + return self.CurrentLayer == self.TargetLayer + else: + return self.CurrentLayer >= self.TargetLayer + + else: + + # if we don't have a current Z, we're not there yet + if self.CurrentZ is None: + return False + + # if we're applying to a single layer, stop if our Z is not identical + if self.IsApplyToSingleLayer: + return self.CurrentZ == self.TargetZ + else: + return self.CurrentZ >= self.TargetZ + + # Marks any current ChangeZ layer defaults in the layer for deletion + @staticmethod + def markChangesForDeletion(layer: str): + return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer) + + # Grabs the current height + def processLayerHeight(self, line: str): + + # stop here if we haven't entered a layer yet + if self.CurrentLayer is None: + return + + # get our gcode command + command = GCodeCommand.getFromLine(line) + + # skip if it's not a command we're interested in + if command is None: + return + + # stop here if this isn't a linear move command + if command.Command != "G0" and command.Command != "G1": + return + + # get our value from the command + current_z = command.getArgumentAsFloat("Z", None) + + # stop here if we don't have a Z value defined, we can't get the height from this command + if current_z is None: + return + + # stop if there's no change + if current_z == self.CurrentZ: + return + + # set our current Z value + self.CurrentZ = current_z + + # if we don't have a layer height yet, set it based on the current Z value + if self.LayerHeight is None: + self.LayerHeight = self.CurrentZ + + # Grabs the current layer number + def processLayerNumber(self, line: str): + + # if this isn't a layer comment, stop here, nothing to update + if ";LAYER:" not in line: + return + + # get our current layer number + current_layer = GCodeCommand.getDirectArgumentAsInt(line, ";LAYER:", None) + + # this should never happen, but if our layer number hasn't changed, stop here + if current_layer == self.CurrentLayer: + return + + # update our current layer + self.CurrentLayer = current_layer + + # Makes any linear move changes and also injects either target or restored values depending on the plugin state + def processLine(self, line: str) -> str: + + # used to change the given line of code + modified_gcode = "" + + # track any values that we may be interested in + self.trackChangeableValues(line) + + # if we're not inside the target layer, simply read the any + # settings we can and revert any ChangeAtZ deletions + if not self.IsInsideTargetLayer: + + # read any settings if we haven't hit our target layer yet + if not self.WasInsideTargetLayer: + self.processSetting(line) + + # if we haven't hit our target yet, leave the defaults as is (unmark them for deletion) + if "[CAZD:DELETE:" in line: + line = line.replace("[CAZD:DELETE:", "[CAZD:") + + # if we're targeting by Z, we want to add our values before the first linear move + if "G1 " in line or "G0 " in line: + modified_gcode += self.getInjectCode() + + # modify our command if we're still inside our target layer, otherwise pass unmodified + if self.IsInsideTargetLayer: + modified_gcode += self.processLinearMove(line) + "\n" + else: + modified_gcode += line + "\n" + + # if we're targetting by layer we want to add our values just after the layer label + if ";LAYER:" in line: + modified_gcode += self.getInjectCode() + + # return our changed code + return modified_gcode + + # Handles any linear moves in the current line + def processLinearMove(self, line: str) -> str: + + # if it's not a linear motion command we're not interested + if not ("G1 " in line or "G0 " in line): + return line + + # always get our original line, otherwise the effect will be cumulative + line = self.getOriginalLine(line) + + # get our command from the line + linear_command = GCodeCommand.getLinearMoveCommand(line) + + # if it's not a linear move, we don't care + if linear_command is None: + return + + # get our linear move parameters + feed_rate = linear_command.Arguments["F"] + x_coord = linear_command.Arguments["X"] + y_coord = linear_command.Arguments["Y"] + z_coord = linear_command.Arguments["Z"] + extrude_length = linear_command.Arguments["E"] + + # set our new line to our old line + new_line = line + + # handle retract length + new_line = self.processRetractLength(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord) + + # handle retract feed rate + new_line = self.processRetractFeedRate(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord) + + # handle print speed adjustments + new_line = self.processPrintSpeed(feed_rate, new_line) + + # set our current extrude position + self.LastE = extrude_length if extrude_length is not None else self.LastE + + # if no changes have been made, stop here + if new_line == line: + return line + + # return our updated command + return self.setOriginalLine(new_line, line) + + # Handles any changes to print speed for the given linear motion command + def processPrintSpeed(self, feed_rate: float, new_line: str) -> str: + + # if we're not setting print speed or we don't have a feed rate, stop here + if "printspeed" not in self.TargetValues or feed_rate is None: + return new_line + + # get our requested print speed + print_speed = int(self.TargetValues["printspeed"]) + + # if they requested no change to print speed (ie: 100%), stop here + if print_speed == 100: + return new_line + + # get our feed rate from the command + feed_rate = GCodeCommand.getDirectArgumentAsFloat(new_line, "F") * (float(print_speed) / 100.0) + + # change our feed rate + return GCodeCommand.replaceDirectArgument(new_line, "F", feed_rate) + + # Handles any changes to retraction length for the given linear motion command + def processRetractLength(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str: + + # if we don't have a retract length in the file we can't add one + if "retractlength" not in self.LastValues or self.LastValues["retractlength"] == 0: + return new_line + + # if we're not changing retraction length, stop here + if "retractlength" not in self.TargetValues: + return new_line + + # retractions are only F (feed rate) and E (extrude), at least in cura + if x_coord is not None or y_coord is not None or z_coord is not None: + return new_line + + # since retractions require both F and E, and we don't have either, we can't process + if feed_rate is None or extrude_length is None: + return new_line + + # stop here if we don't know our last extrude value + if self.LastE is None: + return new_line + + # if there's no change in extrude we have nothing to change + if self.LastE == extrude_length: + return new_line + + # if our last extrude was lower than our current, we're restoring, so skip + if self.LastE < extrude_length: + return new_line + + # get our desired retract length + retract_length = float(self.TargetValues["retractlength"]) + + # subtract the difference between the default and the desired + extrude_length -= (retract_length - self.LastValues["retractlength"]) + + # replace our extrude amount + return GCodeCommand.replaceDirectArgument(new_line, "E", extrude_length) + + # Used for picking out the retract length set by Cura + def processRetractLengthSetting(self, line: str): + + # skip if we're not doing linear retractions + if not self.IsLinearRetraction: + return + + # get our command from the line + linear_command = GCodeCommand.getLinearMoveCommand(line) + + # if it's not a linear move, we don't care + if linear_command is None: + return + + # get our linear move parameters + feed_rate = linear_command.Arguments["F"] + x_coord = linear_command.Arguments["X"] + y_coord = linear_command.Arguments["Y"] + z_coord = linear_command.Arguments["Z"] + extrude_length = linear_command.Arguments["E"] + + # the command we're looking for only has extrude and feed rate + if x_coord is not None or y_coord is not None or z_coord is not None: + return + + # if either extrude or feed is missing we're likely looking at the wrong command + if extrude_length is None or feed_rate is None: + return + + # cura stores the retract length as a negative E just before it starts printing + extrude_length = extrude_length * -1 + + # if it's a negative extrude after being inverted, it's not our retract length + if extrude_length < 0: + return + + # what ever the last negative retract length is it wins + self.LastValues["retractlength"] = extrude_length + + # Handles any changes to retraction feed rate for the given linear motion command + def processRetractFeedRate(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str: + + # skip if we're not doing linear retractions + if not self.IsLinearRetraction: + return new_line + + # if we're not changing retraction length, stop here + if "retractfeedrate" not in self.TargetValues: + return new_line + + # retractions are only F (feed rate) and E (extrude), at least in cura + if x_coord is not None or y_coord is not None or z_coord is not None: + return new_line + + # since retractions require both F and E, and we don't have either, we can't process + if feed_rate is None or extrude_length is None: + return new_line + + # get our desired retract feed rate + retract_feed_rate = float(self.TargetValues["retractfeedrate"]) + + # convert to units/min + retract_feed_rate *= 60 + + # replace our feed rate + return GCodeCommand.replaceDirectArgument(new_line, "F", retract_feed_rate) + + # Used for finding settings in the print file before we process anything else + def processSetting(self, line: str): + + # if we're in layers already we're out of settings + if self.CurrentLayer is not None: + return + + # check our retract length + self.processRetractLengthSetting(line) + + # Sets the flags if we're at the target layer or not + def processTargetLayer(self): + + # skip this line if we're not there yet + if not self.isTargetLayerOrHeight(): + + # flag that we're outside our target layer + self.IsInsideTargetLayer = False + + # skip to the next line + return + + # flip if we hit our target layer + self.WasInsideTargetLayer = True + + # flag that we're inside our target layer + self.IsInsideTargetLayer = True + + # Removes all the ChangeZ layer defaults from the given layer + @staticmethod + def removeMarkedChanges(layer: str) -> str: + return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer) + + # Resets the class contents to defaults + def reset(self): + + self.TargetValues = {} + self.IsApplyToSingleLayer = False + self.LastE = None + self.CurrentZ = None + self.CurrentLayer = None + self.IsTargetByLayer = True + self.TargetLayer = None + self.TargetZ = None + self.LayerHeight = None + self.LastValues = {} + self.IsLinearRetraction = True + self.IsInsideTargetLayer = False + self.IsTargetValuesInjected = False + self.IsLastValuesRestored = False + self.WasInsideTargetLayer = False + self.IsEnabled = True + + # Sets the original GCODE line in a given GCODE command + @staticmethod + def setOriginalLine(line, original) -> str: + return line + ";[CAZO:" + original + ":CAZO]" + + # Tracks the change in gcode values we're interested in + def trackChangeableValues(self, line: str): + + # simulate a print speed command + if ";PRINTSPEED" in line: + line = line.replace(";PRINTSPEED ", "M220 S") + + # simulate a retract feedrate command + if ";RETRACTFEEDRATE" in line: + line = line.replace(";RETRACTFEEDRATE ", "M207 F") + + # simulate a retract length command + if ";RETRACTLENGTH" in line: + line = line.replace(";RETRACTLENGTH ", "M207 S") + + # get our gcode command + command = GCodeCommand.getFromLine(line) + + # stop here if it isn't a G or M command + if command is None: + return + + # handle retract length changes + if command.Command == "M207": + + # get our retract length if provided + if "S" in command.Arguments: + self.LastValues["retractlength"] = command.getArgumentAsFloat("S") + + # get our retract feedrate if provided, convert from mm/m to mm/s + if "F" in command.Arguments: + self.LastValues["retractfeedrate"] = command.getArgumentAsFloat("F") / 60.0 + + # move to the next command + return + + # handle bed temp changes + if command.Command == "M140" or command.Command == "M190": + + # get our bed temp if provided + if "S" in command.Arguments: + self.LastValues["bedTemp"] = command.getArgumentAsFloat("S") + + # move to the next command + return + + # handle extruder temp changes + if command.Command == "M104" or command.Command == "M109": + + # get our tempurature + tempurature = command.getArgumentAsFloat("S") + + # don't bother if we don't have a tempurature + if tempurature is None: + return + + # get our extruder, default to extruder one + extruder = command.getArgumentAsInt("T", None) + + # set our extruder temp based on the extruder + if extruder is None or extruder == 0: + self.LastValues["extruderOne"] = tempurature + + if extruder is None or extruder == 1: + self.LastValues["extruderTwo"] = tempurature + + # move to the next command + return + + # handle fan speed changes + if command.Command == "M106": + + # get our bed temp if provided + if "S" in command.Arguments: + self.LastValues["fanSpeed"] = (command.getArgumentAsInt("S") / 255.0) * 100 + + # move to the next command + return + + # handle flow rate changes + if command.Command == "M221": + + # get our flow rate + tempurature = command.getArgumentAsFloat("S") + + # don't bother if we don't have a flow rate (for some reason) + if tempurature is None: + return + + # get our extruder, default to global + extruder = command.getArgumentAsInt("T", None) + + # set our extruder temp based on the extruder + if extruder is None: + self.LastValues["flowrate"] = tempurature + elif extruder == 1: + self.LastValues["flowrateOne"] = tempurature + elif extruder == 1: + self.LastValues["flowrateTwo"] = tempurature + + # move to the next command + return + + # handle print speed changes + if command.Command == "M220": + + # get our speed if provided + if "S" in command.Arguments: + self.LastValues["speed"] = command.getArgumentAsInt("S") + + # move to the next command + return + + +def debug(): + # get our input file + file = r"C:\Users\Wes\Desktop\Archive\gcode test\emit + single layer\AC_Retraction.gcode" + + # read the whole thing + f = open(file, "r") + gcode = f.read() + f.close() + + # boot up change + caz_instance = ChangeAtZProcessor() + caz_instance.IsTargetByLayer = False + caz_instance.TargetZ = 5 + caz_instance.TargetValues["printspeed"] = 100 + caz_instance.TargetValues["retractfeedrate"] = 60 + + # process gcode + gcode = debug_iteration(gcode, caz_instance) + + # write our file + debug_write(gcode, file + ".1.modified") + + caz_instance.reset() + caz_instance.IsTargetByLayer = False + caz_instance.IsDisplayingChangesToLcd = True + caz_instance.IsApplyToSingleLayer = False + caz_instance.TargetZ = 10.6 + caz_instance.TargetValues["bedTemp"] = 75.111 + caz_instance.TargetValues["printspeed"] = 150 + caz_instance.TargetValues["retractfeedrate"] = 40.555 + caz_instance.TargetValues["retractlength"] = 10.3333 + + # and again + gcode = debug_iteration(gcode, caz_instance) + + # write our file + debug_write(gcode, file + ".2.modified") + + caz_instance.reset() + caz_instance.IsTargetByLayer = False + caz_instance.TargetZ = 15 + caz_instance.IsApplyToSingleLayer = True + caz_instance.TargetValues["bedTemp"] = 80 + caz_instance.TargetValues["printspeed"] = 100 + caz_instance.TargetValues["retractfeedrate"] = 10 + caz_instance.TargetValues["retractlength"] = 0 + caz_instance.TargetValues["extruderOne"] = 100 + caz_instance.TargetValues["extruderTwo"] = 200 + + # and again + gcode = debug_iteration(gcode, caz_instance) + + # write our file + debug_write(gcode, file + ".3.modified") + + +def debug_write(gcode, file): + # write our file + f = open(file, "w") + f.write(gcode) + f.close() + + +def debug_iteration(gcode, caz_instance): + index = 0 + + # break apart the GCODE like cura + layers = re.split(r"^;LAYER:\d+\n", gcode) + + # add the layer numbers back + for layer in layers: + + # if this is the first layer, skip it, basically + if index == 0: + # leave our first layer as is + layers[index] = layer + + # move the cursor + index += 1 + + # skip to the next layer + continue + + layers[index] = ";LAYER:" + str(index - 1) + ";\n" + layer + + return "".join(caz_instance.execute(layers)) + +# debug()