diff --git a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py index f37676a28c..513ae13550 100644 --- a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py +++ b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py @@ -36,6 +36,10 @@ # 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 @@ -44,15 +48,17 @@ # 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 +# 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 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.2.0" + version = "5.2.1" def getSettingDataString(self): return """{ @@ -93,15 +99,21 @@ class ChangeAtZ(Script): "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", + "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" + "keep_value": "Target Layer + Subsequent Layers", + "single_layer": "Target Layer Only" }, "default_value": "keep_value" - }, + }, + "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", @@ -255,16 +267,34 @@ class ChangeAtZ(Script): "maximum_value_warning": "100", "enabled": "j1_Change_fanSpeed" }, - "caz_change_retractfeedrate": { - "label": "Change Retract Feed Rate", - "description": "Changes the retraction feed rate during print (M207)", + "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 (units/s)", - "unit": "units/s", + "description": "New Retract Feed Rate (mm/s)", + "unit": "mm/s", "type": "float", "default_value": 40, "minimum_value": "0", @@ -274,14 +304,15 @@ class ChangeAtZ(Script): }, "caz_change_retractlength": { "label": "Change Retract Length", - "description": "Changes the retraction length during print (M207)", + "description": "Changes the retraction length during print", "type": "bool", - "default_value": false + "default_value": false, + "enabled": "caz_change_retract" }, "caz_retractlength": { "label": "Retract Length", - "description": "New Retract Length (units)", - "unit": "units", + "description": "New Retract Length (mm)", + "unit": "mm", "type": "float", "default_value": 6, "minimum_value": "0", @@ -314,6 +345,12 @@ class ChangeAtZ(Script): self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractfeedrate", "retractfeedrate", "caz_retractfeedrate") self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength") + # 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" @@ -380,109 +417,153 @@ class ChangeAtZ(Script): return default -# 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: - TargetValues = {} - IsApplyToSingleLayer = False - LastE = None - CurrentZ = None - CurrentLayer = None - IsTargetByLayer = True - TargetLayer = None - TargetZ = None - LayerHeight = None - RetractLength = 0 +# This is a utility class for getting details of gcodes from a given line +class GCodeCommand: - # boots up the class with defaults + # The GCode command itself (ex: G10) + Command: str = None, + + # Contains any arguments passed to the command. The key is the argument name, the value is the value of the argument. + Arguments: Dict[str, any] = {} + + # Constructor. Sets up defaults def __init__(self): self.reset() - # Modifies the given GCODE and injects the commands at the various targets - def execute(self, data): - - # indicates if we should inject our defaults or not - inject_defaults = True - - # 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.markDefaultsForDeletion(active_layer) - - # break apart the layer into commands - lines = active_layer.split("\n") - - # evaluate each command individually - for line in lines: - - # skip empty lines - if line.strip() == "": - continue - - # update our layer number if applicable - self.processLayerNumber(line) - - # update our layer height if applicable - self.processLayerHeight(line) - - # skip this line if we're not there yet - if not self.isTargetLayerOrHeight(): - - # read any settings we might need - 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:") - - # set our line - modified_gcode += line + "\n" - - # skip to the next line - continue - - # inject our defaults before linear motion commands - if inject_defaults and ("G1" in line or "G0" in line): - - # inject the defaults - modified_gcode += self.getTargetDefaults() + "\n" - - # mark that we've injected the defaults - inject_defaults = False - - # append to our modified layer - modified_gcode += self.processLinearMove(line) + "\n" - - # inject our defaults after the layer indicator - if inject_defaults and ";LAYER:" in line: - - # inject the defaults - modified_gcode += self.getTargetDefaults() + "\n" - - # mark that we've injected the defaults - inject_defaults = False - - # remove any marked defaults - modified_gcode = self.removeMarkedTargetDefaults(modified_gcode) - - # append our modified line - data[index] = modified_gcode - - index += 1 - return data - - # Converts the command parameter to a float or returns the default + # Gets a GCode Command from the given single line of GCode @staticmethod - def getFloatValue(line, key, default=None): + 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: List[str] = 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 + + # 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 + + # remove the command from the pieces + del command_pieces[0] + + # iterate and index all of our parameters + for param in command_pieces: + + # get the first character of the parameter, which is the name + param_name:str = param[0] + + # get the value of the parameter (the rest of the string + param_value:str = None + + # get our value if we have one + if len(param) > 1: + param_value = param[1:] + + # index the argument + command.Arguments[param_name] = param_value + + # 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: + + # 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: + # 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 = ChangeAtZProcessor.getValue(line, key, default) + value = GCodeCommand.getDirectArgument(line, key, default) # stop here if it's the default if value == default: @@ -495,10 +576,10 @@ class ChangeAtZProcessor: # Converts the command parameter to a int or returns the default @staticmethod - def getIntValue(line, key, default=None): + def getDirectArgumentAsInt(line: str, key: str, default: int = None) -> int: # get the value from the command - value = ChangeAtZProcessor.getValue(line, key, default) + value = GCodeCommand.getDirectArgument(line, key, default) # stop here if it's the default if value == default: @@ -509,21 +590,330 @@ class ChangeAtZProcessor: except: return default - # Handy function for reading a linear move command - def getLinearMoveParams(self, line): + # 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) - # get our motion parameters - feed_rate = self.getFloatValue(line, "F", None) - x_coord = self.getFloatValue(line, "X", None) - y_coord = self.getFloatValue(line, "Y", None) - z_coord = self.getFloatValue(line, "Z", None) - extrude_length = self.getFloatValue(line, "E", None) + # Resets the model back to defaults + def reset(self): + self.Command = None + self.Arguments = {} - return extrude_length, feed_rate, x_coord, y_coord, z_coord + +# 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: float = None + + # Holds our current layer number + CurrentLayer: int = None + + # Indicates if we're only supposed to apply our settings to a single layer or multiple layers + IsApplyToSingleLayer: bool = False + + # Indicates if this should emit the changes as they happen to the LCD + IsDisplayingChangesToLcd: bool = False + + # Indicates if we're processing inside the target layer or not + IsInsideTargetLayer: bool = False + + # Indicates if we have restored the previous values from before we started our pass + IsLastValuesRestored: bool = False + + # Indicates if the user has opted for linear move retractions or firmware retractions + IsLinearRetraction: bool = True + + # Indicates if we're targetting by layer or height value + IsTargetByLayer: bool = True + + # Indicates if we have injected our changed values for the given layer yet + IsTargetValuesInjected: bool = False + + # Holds the last extrusion value, used with detecting when a retraction is made + LastE: float = None + + # An index of our gcodes which we're monitoring + LastValues: Dict[str, any] = {} + + # The detected layer height from the gcode + LayerHeight: float = None + + # The target layer + TargetLayer: int = None + + # Holds the values the user has requested to change + TargetValues: Dict[str, any] = {} + + # The target height in mm + TargetZ: float = None + + # Used to track if we've been inside our target layer yet + WasInsideTargetLayer: bool = 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): + + # our layer cursor + index: int = 0 + + for active_layer in data: + + # will hold our updated gcode + modified_gcode: str = "" + + # mark all the defaults for deletion + active_layer = self.markChangesForDeletion(active_layer) + + # break apart the layer into commands + lines: List[str] = active_layer.split("\n") + + # evaluate each command individually + for line in lines: + + # trim or command + line = line.strip() + + # skip empty lines + if len(line) == 0: + continue + + if "Z10.8" in line: + derp = True + + # 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: Dict[str, any] = {} + + # 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: List[str] = [] + + # 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: List[str] = 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: List[str] = [] + + # 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): + def getOriginalLine(line: str) -> str: # get the change at z original (cazo) details original_line = re.search(r"\[CAZO:(.*?):CAZO\]", line) @@ -534,98 +924,14 @@ class ChangeAtZProcessor: return original_line.group(1) - # Builds the layer defaults based on the settings and returns the relevant GCODE lines - def getTargetDefaults(self): + # Builds the target layer settings based on the specified values and returns the relevant GCODE lines + def getTargetValues(self) -> str: - # will hold all the default settings for the target layer - defaults = [] - - # used to trim other defaults - defaults.append(";[CAZD:") - - # looking for wait for bed temp - if "bedTemp" in self.TargetValues: - defaults.append("M190 S" + str(self.TargetValues["bedTemp"])) - - # set our extruder one temp (if specified) - if "extruderOne" in self.TargetValues: - defaults.append("M109 S" + str(self.TargetValues["extruderOne"]) + " T0") - - # set our extruder two temp (if specified) - if "extruderTwo" in self.TargetValues: - defaults.append("M109 S" + str(self.TargetValues["extruderTwo"]) + " T1") - - # set our fan speed - if "fanSpeed" in self.TargetValues: - - # convert our fan speed percentage to PWM - fan_speed = int((float(self.TargetValues["fanSpeed"]) / 100.0) * 255) - - # add our fan speed to the defaults - defaults.append("M106 S" + str(fan_speed)) - - # set global flow rate - if "flowrate" in self.TargetValues: - defaults.append("M221 S" + str(self.TargetValues["flowrate"])) - - # set extruder 0 flow rate - if "flowrateOne" in self.TargetValues: - defaults.append("M221 S" + str(self.TargetValues["flowrateOne"]) + " T0") - - # set second extruder flow rate - if "flowrateTwo" in self.TargetValues: - defaults.append("M221 S" + str(self.TargetValues["flowrateTwo"]) + " T1") - - # set feedrate percentage - if "speed" in self.TargetValues: - defaults.append("M220 S" + str(self.TargetValues["speed"]) + " T1") - - # set print rate percentage - if "printspeed" in self.TargetValues: - defaults.append(";PRINTSPEED " + str(self.TargetValues["printspeed"]) + "") - - # set retract rate - if "retractfeedrate" in self.TargetValues: - defaults.append(";RETRACTFEEDRATE " + str(self.TargetValues["retractfeedrate"]) + "") - - # set retract length - if "retractlength" in self.TargetValues: - defaults.append(";RETRACTLENGTH " + str(self.TargetValues["retractlength"]) + "") - - # used to trim other defaults - defaults.append(";:CAZD]") - - # if there are no defaults, stop here - if len(defaults) == 2: - return "" - - # return our default block for this layer - return "\n".join(defaults) - - # Allows retrieving values from the given GCODE line - @staticmethod - def getValue(line, key, default=None): - - 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 - - sub_part = line[line.find(key) + len(key):] # allows for string lengths larger than 1 - if ";ChangeAtZ" in key: - m = re.search("^[0-4]", sub_part) - elif ";LAYER:" in key: - m = re.search("^[+-]?[0-9]*", sub_part) - else: - # 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 float(m.group(0)) - except: - return default + # 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): + def isTargetLayerOrHeight(self) -> bool: # target selected by layer no. if self.IsTargetByLayer: @@ -654,29 +960,33 @@ class ChangeAtZProcessor: # Marks any current ChangeZ layer defaults in the layer for deletion @staticmethod - def markDefaultsForDeletion(layer): + def markChangesForDeletion(layer: str): return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer) # Grabs the current height - def processLayerHeight(self, line): + def processLayerHeight(self, line: str): # stop here if we haven't entered a layer yet if self.CurrentLayer is None: return - # expose the main command - line_no_comments = self.stripComments(line) + # get our gcode command + command = GCodeCommand.getFromLine(line) - # stop here if this isn't a linear move command - if not ("G1" in line_no_comments or "G0" in line_no_comments): + # skip if it's not a command we're interested in + if command is None: return - # stop here if we don't have a Z value defined, we can't get the height from this command - if "Z" not in line_no_comments: + # 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 = self.getFloatValue(line_no_comments, "Z", None) + 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: @@ -690,14 +1000,14 @@ class ChangeAtZProcessor: self.LayerHeight = self.CurrentZ # Grabs the current layer number - def processLayerNumber(self, line): + 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 = self.getIntValue(line, ";LAYER:", None) + 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: @@ -706,21 +1016,70 @@ class ChangeAtZProcessor: # 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: str = "" + + # 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): + 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): + 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 the details from our linear move command - extrude_length, feed_rate, x_coord, y_coord, z_coord = self.getLinearMoveParams(line) + # get our command from the line + linear_command: Optional[GCodeCommand] = 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: float = linear_command.Arguments["F"] + x_coord: float = linear_command.Arguments["X"] + y_coord: float = linear_command.Arguments["Y"] + z_coord: float = linear_command.Arguments["Z"] + extrude_length: float = linear_command.Arguments["E"] # set our new line to our old line - new_line = line + new_line: str = line # handle retract length new_line = self.processRetractLength(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord) @@ -742,30 +1101,30 @@ class ChangeAtZProcessor: return self.setOriginalLine(new_line, line) # Handles any changes to print speed for the given linear motion command - def processPrintSpeed(self, feed_rate, new_line): + 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"]) + print_speed: int = 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 = float(self.getValue(new_line, "F")) * (float(print_speed) / 100.0) + feed_rate: float = GCodeCommand.getDirectArgumentAsFloat(new_line, "F") * (float(print_speed) / 100.0) # change our feed rate - return self.replaceParameter(new_line, "F", 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, feed_rate, new_line, x_coord, y_coord, z_coord): + 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 self.RetractLength == 0: + if "retractlength" not in self.LastValues or self.LastValues["retractlength"] == 0: return new_line # if we're not changing retraction length, stop here @@ -796,20 +1155,31 @@ class ChangeAtZProcessor: retract_length = float(self.TargetValues["retractlength"]) # subtract the difference between the default and the desired - extrude_length -= (retract_length - self.RetractLength) + extrude_length -= (retract_length - self.LastValues["retractlength"]) # replace our extrude amount - return self.replaceParameter(new_line, "E", extrude_length) + return GCodeCommand.replaceDirectArgument(new_line, "E", extrude_length) # Used for picking out the retract length set by Cura - def processRetractLengthSetting(self, line): + def processRetractLengthSetting(self, line: str): - # if it's not a linear move, we don't care - if "G0" not in line and "G1" not in line: + # skip if we're not doing linear retractions + if not self.IsLinearRetraction: return - # get the details from our linear move command - extrude_length, feed_rate, x_coord, y_coord, z_coord = self.getLinearMoveParams(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"] # 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: @@ -827,10 +1197,14 @@ class ChangeAtZProcessor: return # what ever the last negative retract length is it wins - self.RetractLength = extrude_length + self.LastValues["retractlength"] = extrude_length # Handles any changes to retraction feed rate for the given linear motion command - def processRetractFeedRate(self, extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord): + 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: @@ -851,10 +1225,10 @@ class ChangeAtZProcessor: retract_feed_rate *= 60 # replace our feed rate - return self.replaceParameter(new_line, "F", retract_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): + def processSetting(self, line: str): # if we're in layers already we're out of settings if self.CurrentLayer is not None: @@ -863,16 +1237,29 @@ class ChangeAtZProcessor: # 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 removeMarkedTargetDefaults(layer): + def removeMarkedChanges(layer: str) -> str: return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer) - # Easy function for replacing any GCODE parameter variable in a given GCODE command - @staticmethod - def replaceParameter(line, key, value): - return re.sub(r"(^|\s)" + key + r"[\d\.]+(\s|$)", r"\1" + key + str(value) + r"\2", line) - # Resets the class contents to defaults def reset(self): @@ -885,22 +1272,135 @@ class ChangeAtZProcessor: self.TargetLayer = None self.TargetZ = None self.LayerHeight = None - self.RetractLength = 0 + self.LastValues = {} + self.IsLinearRetraction = True + self.IsInsideTargetLayer = False + self.IsTargetValuesInjected = False + self.IsLastValuesRestored = False + self.WasInsideTargetLayer = False # Sets the original GCODE line in a given GCODE command @staticmethod - def setOriginalLine(line, original): + def setOriginalLine(line, original) -> str: return line + ";[CAZO:" + original + ":CAZO]" - # Removes the gcode comments from a given gcode command - @staticmethod - def stripComments(line): - return re.sub(r";.*?$", "", line).strip() + # 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: Optional[GCodeCommand] = 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: float = 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: float = 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"PATH_TO_SOME_GCODE.gcode" + file = r"C:\Users\Wes\Desktop\Archive\gcode test\emit + single layer\AC_Retraction.gcode" # read the whole thing f = open(file, "r") @@ -922,7 +1422,9 @@ def debug(): caz_instance.reset() caz_instance.IsTargetByLayer = False - caz_instance.TargetZ = 10.5 + 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 @@ -937,6 +1439,7 @@ def debug(): 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