diff --git a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py index cdbb4a79ef..9e93600fbf 100644 --- a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py +++ b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py @@ -4,496 +4,954 @@ # 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. -## 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 +# M605/606 to save and recall material settings on the UM2 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__() - def getSettingDataString(self): - return """{ - "name":"ChangeAtZ """ + self.version + """ (Experimental)", - "key":"ChangeAtZ", - "metadata": {}, - "version": 2, - "settings": - { - "a_trigger": - { - "label": "Trigger", - "description": "Trigger at height or at layer no.", - "type": "enum", - "options": {"height":"Height","layer_no":"Layer No."}, - "default_value": "height" - }, - "b_targetZ": - { - "label": "Change Height", - "description": "Z height to change at", - "unit": "mm", - "type": "float", - "default_value": 5.0, - "minimum_value": "0", - "minimum_value_warning": "0.1", - "maximum_value_warning": "230", - "enabled": "a_trigger == 'height'" - }, - "b_targetL": - { - "label": "Change Layer", - "description": "Layer no. to change at", - "unit": "", - "type": "int", - "default_value": 1, - "minimum_value": "-100", - "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", - "type": "enum", - "options": {"keep_value":"Keep value","single_layer":"Single Layer"}, - "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": - { - "label": "Change Speed", - "description": "Select if total speed (print and travel) has to be changed", - "type": "bool", - "default_value": false - }, - "e2_speed": - { - "label": "Speed", - "description": "New total speed (print and travel)", - "unit": "%", - "type": "int", - "default_value": 100, - "minimum_value": "1", - "minimum_value_warning": "10", - "maximum_value_warning": "200", - "enabled": "e1_Change_speed" - }, - "f1_Change_printspeed": - { - "label": "Change Print Speed", - "description": "Select if print speed has to be changed", - "type": "bool", - "default_value": false - }, - "f2_printspeed": - { - "label": "Print Speed", - "description": "New print speed", - "unit": "%", - "type": "int", - "default_value": 100, - "minimum_value": "1", - "minimum_value_warning": "10", - "maximum_value_warning": "200", - "enabled": "f1_Change_printspeed" - }, - "g1_Change_flowrate": - { - "label": "Change Flow Rate", - "description": "Select if flow rate has to be changed", - "type": "bool", - "default_value": false - }, - "g2_flowrate": - { - "label": "Flow Rate", - "description": "New Flow rate", - "unit": "%", - "type": "int", - "default_value": 100, - "minimum_value": "1", - "minimum_value_warning": "10", - "maximum_value_warning": "200", - "enabled": "g1_Change_flowrate" - }, - "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": - { - "label": "Flow Rate One", - "description": "New Flow rate Extruder 1", - "unit": "%", - "type": "int", - "default_value": 100, - "minimum_value": "1", - "minimum_value_warning": "10", - "maximum_value_warning": "200", - "enabled": "g3_Change_flowrateOne" - }, - "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": - { - "label": "Flow Rate two", - "description": "New Flow rate Extruder 2", - "unit": "%", - "type": "int", - "default_value": 100, - "minimum_value": "1", - "minimum_value_warning": "10", - "maximum_value_warning": "200", - "enabled": "g5_Change_flowrateTwo" - }, - "h1_Change_bedTemp": - { - "label": "Change Bed Temp", - "description": "Select if Bed Temperature has to be changed", - "type": "bool", - "default_value": false - }, - "h2_bedTemp": - { - "label": "Bed Temp", - "description": "New Bed Temperature", - "unit": "C", - "type": "float", - "default_value": 60, - "minimum_value": "0", - "minimum_value_warning": "30", - "maximum_value_warning": "120", - "enabled": "h1_Change_bedTemp" - }, - "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": - { - "label": "Extruder 1 Temp", - "description": "New First Extruder Temperature", - "unit": "C", - "type": "float", - "default_value": 190, - "minimum_value": "0", - "minimum_value_warning": "160", - "maximum_value_warning": "250", - "enabled": "i1_Change_extruderOne" - }, - "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": - { - "label": "Extruder 2 Temp", - "description": "New Second Extruder Temperature", - "unit": "C", - "type": "float", - "default_value": 190, - "minimum_value": "0", - "minimum_value_warning": "160", - "maximum_value_warning": "250", - "enabled": "i3_Change_extruderTwo" - }, - "j1_Change_fanSpeed": - { - "label": "Change Fan Speed", - "description": "Select if Fan Speed has to be changed", - "type": "bool", - "default_value": false - }, - "j2_fanSpeed": - { - "label": "Fan Speed", - "description": "New Fan Speed (0-255)", - "unit": "PWM", - "type": "int", - "default_value": 255, - "minimum_value": "0", - "minimum_value_warning": "15", - "maximum_value_warning": "255", - "enabled": "j1_Change_fanSpeed" - } - } - }""" + version = "5.2.0" - 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 getSettingDataString(self): + return """{ + "name": "ChangeAtZ """ + self.version + """(Experimental)", + "key": "ChangeAtZ", + "metadata": {}, + "version": 2, + "settings": { + "a_trigger": { + "label": "Trigger", + "description": "Trigger at height or at layer no.", + "type": "enum", + "options": { + "height": "Height", + "layer_no": "Layer No." + }, + "default_value": "height" + }, + "b_targetZ": { + "label": "Change Height", + "description": "Z height to change at", + "unit": "mm", + "type": "float", + "default_value": 5.0, + "minimum_value": "0", + "minimum_value_warning": "0.1", + "maximum_value_warning": "230", + "enabled": "a_trigger == 'height'" + }, + "b_targetL": { + "label": "Change Layer", + "description": "Layer no. to change at", + "unit": "", + "type": "int", + "default_value": 1, + "minimum_value": "-100", + "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", + "type": "enum", + "options": { + "keep_value": "Keep value", + "single_layer": "Single Layer" + }, + "default_value": "keep_value" + }, + "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": { + "label": "Speed", + "description": "New total speed (print and travel)", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "e1_Change_speed" + }, + "f1_Change_printspeed": { + "label": "Change Print Speed", + "description": "Select if print speed has to be changed", + "type": "bool", + "default_value": false + }, + "f2_printspeed": { + "label": "Print Speed", + "description": "New print speed", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "f1_Change_printspeed" + }, + "g1_Change_flowrate": { + "label": "Change Flow Rate", + "description": "Select if flow rate has to be changed", + "type": "bool", + "default_value": false + }, + "g2_flowrate": { + "label": "Flow Rate", + "description": "New Flow rate", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g1_Change_flowrate" + }, + "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": { + "label": "Flow Rate One", + "description": "New Flow rate Extruder 1", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g3_Change_flowrateOne" + }, + "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": { + "label": "Flow Rate two", + "description": "New Flow rate Extruder 2", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g5_Change_flowrateTwo" + }, + "h1_Change_bedTemp": { + "label": "Change Bed Temp", + "description": "Select if Bed Temperature has to be changed", + "type": "bool", + "default_value": false + }, + "h2_bedTemp": { + "label": "Bed Temp", + "description": "New Bed Temperature", + "unit": "C", + "type": "float", + "default_value": 60, + "minimum_value": "0", + "minimum_value_warning": "30", + "maximum_value_warning": "120", + "enabled": "h1_Change_bedTemp" + }, + "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": { + "label": "Extruder 1 Temp", + "description": "New First Extruder Temperature", + "unit": "C", + "type": "float", + "default_value": 190, + "minimum_value": "0", + "minimum_value_warning": "160", + "maximum_value_warning": "250", + "enabled": "i1_Change_extruderOne" + }, + "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": { + "label": "Extruder 2 Temp", + "description": "New Second Extruder Temperature", + "unit": "C", + "type": "float", + "default_value": 190, + "minimum_value": "0", + "minimum_value_warning": "160", + "maximum_value_warning": "250", + "enabled": "i3_Change_extruderTwo" + }, + "j1_Change_fanSpeed": { + "label": "Change Fan Speed", + "description": "Select if Fan Speed has to be changed", + "type": "bool", + "default_value": false + }, + "j2_fanSpeed": { + "label": "Fan Speed", + "description": "New Fan Speed (0-100)", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "0", + "minimum_value_warning": "0", + "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)", + "type": "bool", + "default_value": false + }, + "caz_retractfeedrate": { + "label": "Retract Feed Rate", + "description": "New Retract Feed Rate (units/s)", + "unit": "units/s", + "type": "int", + "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 (M207)", + "type": "bool", + "default_value": false + }, + "caz_retractlength": { + "label": "Retract Length", + "description": "New Retract Length (units)", + "unit": "units", + "type": "int", + "default_value": 6, + "minimum_value": "0", + "minimum_value_warning": "0", + "maximum_value_warning": "20", + "enabled": "caz_change_retractlength" + } + } + }""" - 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 + def __init__(self): + super().__init__() - if self.getSettingValueByKey("a_trigger") == "layer_no": - targetL_i = int(self.getSettingValueByKey("b_targetL")) - targetZ = 100000 - else: - targetL_i = -100000 - targetZ = self.getSettingValueByKey("b_targetZ") - index = 0 - for active_layer in data: - modified_gcode = "" - lines = active_layer.split("\n") - for line in lines: - if line.strip() == "": - 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]) - data[index] = modified_gcode - index += 1 - return data + def execute(self, data): + + 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.setIntSettingIfEnabled(caz_instance, "h1_Change_bedTemp", "bedTemp", "h2_bedTemp") + self.setIntSettingIfEnabled(caz_instance, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne") + self.setIntSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo") + self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed") + self.setIntSettingIfEnabled(caz_instance, "caz_change_retractfeedrate", "retractfeedrate", "caz_retractfeedrate") + self.setIntSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength") + + # 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.getIntSettingByKey("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 + + # 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 + + +# 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 + + # 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): + + # 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(line): + + # 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 + @staticmethod + def getFloatValue(line, key, default=None): + + # get the value from the command + value = ChangeAtZProcessor.getValue(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 getIntValue(line, key, default=None): + + # get the value from the command + value = ChangeAtZProcessor.getValue(line, key, default) + + # stop here if it's the default + if value == default: + return value + + try: + return int(value) + except: + return default + + # Handy function for reading a linear move command + def getLinearMoveParams(self, 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) + + return extrude_length, feed_rate, x_coord, y_coord, z_coord + + # Returns the unmodified GCODE line from previous ChangeZ edits + @staticmethod + def getOriginalLine(line): + + # 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 layer defaults based on the settings and returns the relevant GCODE lines + def getTargetDefaults(self): + + # 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 temps + if "extruderOne" in self.TargetValues: + defaults.append("M109 S" + str(self.TargetValues["extruderOne"])) + elif "extruderTwo" in self.TargetValues: + defaults.append("M109 S" + str(self.TargetValues["extruderTwo"])) + + # 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 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 + + 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 + + # Determines if the current line is at or below the target required to start modifying + def isTargetLayerOrHeight(self, line): + + # 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 markDefaultsForDeletion(layer): + return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer) + + # Grabs the current height + def processLayerHeight(self, line): + + # 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) + + # stop here if this isn't a linear move command + if not ("G1" in line_no_comments or "G0" in line_no_comments): + 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: + return + + # get our value from the command + current_z = self.getIntValue(line_no_comments, "Z", None) + + # 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): + + # 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) + + # 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 + + # Handles any linear moves in the current line + def processLinearMove(self, line): + + # 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 the details from our linear move command + extrude_length, feed_rate, x_coord, y_coord, z_coord = self.getLinearMoveParams(line) + + # 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, new_line): + + # 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 = float(self.getValue(new_line, "F")) * (float(print_speed) / 100.0) + + # change our feed rate + return self.replaceParameter(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): + + # if we don't have a retract length in the file we can't add one + if self.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 = int(self.TargetValues["retractlength"]) + + # subtract the difference between the default and the desired + extrude_length -= (retract_length - self.RetractLength) + + # replace our extrude amount + return self.replaceParameter(new_line, "E", extrude_length) + + # Used for picking out the retract length set by Cura + def processRetractLengthSetting(self, line): + + # if it's not a linear move, we don't care + if "G0" not in line and "G1" not in line: + return + + # get the details from our linear move command + extrude_length, feed_rate, x_coord, y_coord, z_coord = self.getLinearMoveParams(line) + + # 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.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): + + # 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 = int(self.TargetValues["retractfeedrate"]) + + # convert to units/min + retract_feed_rate *= 60 + + # replace our feed rate + return self.replaceParameter(new_line, "F", retract_feed_rate) + + # Used for finding settings in the print file before we process anything else + def processSetting(self, line): + + # 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) + + # Removes all the ChangeZ layer defaults from the given layer + @staticmethod + def removeMarkedTargetDefaults(layer): + 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): + + 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.RetractLength = 0 + + # Sets the original GCODE line in a given GCODE command + @staticmethod + def setOriginalLine(line, original): + return line + ";[CAZO:" + original + ":CAZO]" + + # Removes the gcode comments from a given gcode command + @staticmethod + def stripComments(line): + return re.sub(r";.*?$", "", line).strip() + + +def debug(): + # get our input file + file = r"PATH_TO_SOME_GCODE.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.TargetZ = 10 + caz_instance.TargetValues["bedTemp"] = 75 + caz_instance.TargetValues["printspeed"] = 150 + caz_instance.TargetValues["retractfeedrate"] = 40 + caz_instance.TargetValues["retractlength"] = 10 + + # 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.TargetValues["bedTemp"] = 80 + caz_instance.TargetValues["printspeed"] = 100 + caz_instance.TargetValues["retractfeedrate"] = 10 + caz_instance.TargetValues["retractlength"] = 0 + + # 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()