From 943d04a734d5bb49ab0e20941a492575d4fefc9e Mon Sep 17 00:00:00 2001 From: novamxd Date: Mon, 17 Feb 2020 22:58:39 -0600 Subject: [PATCH] * Revamped ChangeAtZ Added additional option for disabling all retract options Added more comments. Added support for firmware based retractions. Added support for outputting changes to LCD (untested). Added type hints to most functions and variables. Created GCodeCommand class for better detection of G1 vs G10 or G11 commands, and accessing arguments. Fixed issue of properly restoring previous values in single layer option. Improved wording of Single Layer vs Keep Layer to better reflect what was happening. Moved most GCode methods to GCodeCommand class. --- .../PostProcessingPlugin/scripts/ChangeAtZ.py | 1025 ++++++++++++----- 1 file changed, 764 insertions(+), 261 deletions(-) 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