1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474 |
- # ChangeAtZ script - Change printing parameters at a given height
- # This script is the successor of the TweakAtZ plugin for legacy Cura.
- # It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
- # 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:
- # 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 Ghostkeeper (Ultimaker), rubend@tutanota.com, to debug.
- # Modified by Wes Hanney, https://github.com/novamxd, Retract Length + Speed, Clean up
- # Modified by Alex Jaxon, https://github.com/legend069, Added option to modify Build Volume Temperature
- # 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 unknown 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 ChangeAtZ
- # 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.
- # V5.3.0 Alex Jaxon, Added option to modify Build Volume Temperature keeping current format
- #
- # Uses -
- # M220 S<factor in percent> - set speed factor override percentage
- # M221 S<factor in percent> - set flow factor override percentage
- # M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
- # M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
- # M140 S<temp> - set bed target temperature
- # M106 S<PWM> - set fan speed to target speed <S>
- # M207 S<mm> F<mm/m> - set the retract length <S> or feed rate <F>
- # M117 - output the current changes
- from typing import List, Dict
- from ..Script import Script
- import re
- # this was broken up into a separate class so the main ChangeAtZ script could be debugged outside of Cura
- class ChangeAtZ(Script):
- version = "5.3.0"
- def getSettingDataString(self):
- return """{
- "name": "ChangeAtZ """ + self.version + """(Experimental)",
- "key": "ChangeAtZ",
- "metadata": {},
- "version": 2,
- "settings": {
- "caz_enabled": {
- "label": "Enabled",
- "description": "Allows adding multiple ChangeAtZ mods and disabling them as needed.",
- "type": "bool",
- "default_value": true
- },
- "a_trigger": {
- "label": "Trigger",
- "description": "Trigger at height or at layer no.",
- "type": "enum",
- "options": {
- "height": "Height",
- "layer_no": "Layer No."
- },
- "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": "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": "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",
- "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"
- },
- "h1_Change_buildVolumeTemperature": {
- "label": "Change Build Volume Temperature",
- "description": "Select if Build Volume Temperature has to be changed",
- "type": "bool",
- "default_value": false
- },
- "h2_buildVolumeTemperature": {
- "label": "Build Volume Temperature",
- "description": "New Build Volume Temperature",
- "unit": "C",
- "type": "float",
- "default_value": 20,
- "minimum_value": "0",
- "minimum_value_warning": "10",
- "maximum_value_warning": "50",
- "enabled": "h1_Change_buildVolumeTemperature"
- },
- "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_retract": {
- "label": "Change Retraction",
- "description": "Indicates you would like to modify retraction properties.",
- "type": "bool",
- "default_value": false
- },
- "caz_retractstyle": {
- "label": "Retract Style",
- "description": "Specify if you're using firmware retraction or linear move based retractions. Check your printer settings to see which you're using.",
- "type": "enum",
- "options": {
- "linear": "Linear Move",
- "firmware": "Firmware"
- },
- "default_value": "linear",
- "enabled": "caz_change_retract"
- },
- "caz_change_retractfeedrate": {
- "label": "Change Retract Feed Rate",
- "description": "Changes the retraction feed rate during print",
- "type": "bool",
- "default_value": false,
- "enabled": "caz_change_retract"
- },
- "caz_retractfeedrate": {
- "label": "Retract Feed Rate",
- "description": "New Retract Feed Rate (mm/s)",
- "unit": "mm/s",
- "type": "float",
- "default_value": 40,
- "minimum_value": "0",
- "minimum_value_warning": "0",
- "maximum_value_warning": "100",
- "enabled": "caz_change_retractfeedrate"
- },
- "caz_change_retractlength": {
- "label": "Change Retract Length",
- "description": "Changes the retraction length during print",
- "type": "bool",
- "default_value": false,
- "enabled": "caz_change_retract"
- },
- "caz_retractlength": {
- "label": "Retract Length",
- "description": "New Retract Length (mm)",
- "unit": "mm",
- "type": "float",
- "default_value": 6,
- "minimum_value": "0",
- "minimum_value_warning": "0",
- "maximum_value_warning": "20",
- "enabled": "caz_change_retractlength"
- }
- }
- }"""
- def __init__(self):
- super().__init__()
- 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.setFloatSettingIfEnabled(caz_instance, "h1_Change_bedTemp", "bedTemp", "h2_bedTemp")
- self.setFloatSettingIfEnabled(caz_instance, "h1_Change_buildVolumeTemperature", "buildVolumeTemperature", "h2_buildVolumeTemperature")
- self.setFloatSettingIfEnabled(caz_instance, "i1_Change_extruderOne", "extruderOne", "i2_extruderOne")
- self.setFloatSettingIfEnabled(caz_instance, "i3_Change_extruderTwo", "extruderTwo", "i4_extruderTwo")
- self.setIntSettingIfEnabled(caz_instance, "j1_Change_fanSpeed", "fanSpeed", "j2_fanSpeed")
- self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractfeedrate", "retractfeedrate", "caz_retractfeedrate")
- self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength")
- # is this mod enabled?
- caz_instance.enabled = self.getSettingValueByKey("caz_enabled")
- # are we emitting data to the LCD?
- caz_instance.displayChangesToLcd = self.getSettingValueByKey("caz_output_to_display")
- # are we doing linear move retractions?
- caz_instance.linearRetraction = self.getSettingValueByKey("caz_retractstyle") == "linear"
- # see if we're applying to a single layer or to all layers hence forth
- caz_instance.applyToSingleLayer = self.getSettingValueByKey("c_behavior") == "single_layer"
- # used for easy reference of layer or height targeting
- caz_instance.targetByLayer = self.getSettingValueByKey("a_trigger") == "layer_no"
- # change our target based on what we're targeting
- caz_instance.targetLayer = self.getIntSettingByKey("b_targetL", None)
- caz_instance.targetZ = self.getFloatSettingByKey("b_targetZ", None)
- # run our script
- return caz_instance.execute(data)
- # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
- def setIntSettingIfEnabled(self, caz_instance, trigger, target, setting):
- # stop here if our trigger isn't enabled
- if not self.getSettingValueByKey(trigger):
- return
- # get our value from the settings
- value = self.getIntSettingByKey(setting, None)
- # skip if there's no value or we can't interpret it
- if value is None:
- return
- # set our value in the target settings
- caz_instance.targetValues[target] = value
- # Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
- def setFloatSettingIfEnabled(self, caz_instance, trigger, target, setting):
- # stop here if our trigger isn't enabled
- if not self.getSettingValueByKey(trigger):
- return
- # get our value from the settings
- value = self.getFloatSettingByKey(setting, None)
- # skip if there's no value or we can't interpret it
- if value is None:
- return
- # set our value in the target settings
- caz_instance.targetValues[target] = value
- # Returns the given settings value as an integer or the default if it cannot parse it
- def getIntSettingByKey(self, key, default):
- # change our target based on what we're targeting
- try:
- return int(self.getSettingValueByKey(key))
- except:
- return default
- # Returns the given settings value as an integer or the default if it cannot parse it
- def getFloatSettingByKey(self, key, default):
- # change our target based on what we're targeting
- try:
- return float(self.getSettingValueByKey(key))
- except:
- return default
- # This is a utility class for getting details of gcodes from a given line
- class GCodeCommand:
- # The GCode command itself (ex: G10)
- command = None,
- # Contains any arguments passed to the command. The key is the argument name, the value is the value of the argument.
- arguments = {}
- # Contains the components of the command broken into pieces
- components = []
- # Constructor. Sets up defaults
- def __init__(self):
- self.reset()
- # Gets a GCode Command from the given single line of GCode
- @staticmethod
- def getFromLine(line: str):
- # obviously if we don't have a command, we can't return anything
- if line is None or len(line) == 0:
- return None
- # we only support G or M commands
- if line[0] != "G" and line[0] != "M":
- return None
- # remove any comments
- line = re.sub(r";.*$", "", line)
- # break into the individual components
- command_pieces = line.strip().split(" ")
- # our return command details
- command = GCodeCommand()
- # stop here if we don't even have something to interpret
- if len(command_pieces) == 0:
- return None
- # stores all the components of the command within the class for later
- command.components = command_pieces
- # set the actual command
- command.command = command_pieces[0]
- # stop here if we don't have any parameters
- if len(command_pieces) == 1:
- return None
- # return our indexed command
- return command
- # Handy function for reading a linear move command
- @staticmethod
- def getLinearMoveCommand(line: str):
- # get our command from the line
- linear_command = GCodeCommand.getFromLine(line)
- # if it's not a linear move, we don't care
- if linear_command is None or (linear_command.command != "G0" and linear_command.command != "G1"):
- return None
- # convert our values to floats (or defaults)
- linear_command.arguments["F"] = linear_command.getArgumentAsFloat("F", None)
- linear_command.arguments["X"] = linear_command.getArgumentAsFloat("X", None)
- linear_command.arguments["Y"] = linear_command.getArgumentAsFloat("Y", None)
- linear_command.arguments["Z"] = linear_command.getArgumentAsFloat("Z", None)
- linear_command.arguments["E"] = linear_command.getArgumentAsFloat("E", None)
- # return our new command
- return linear_command
- # Gets the value of a parameter or returns the default if there is none
- def getArgument(self, name: str, default: str = None) -> str:
- # parse our arguments (only happens once)
- self.parseArguments()
- # if we don't have the parameter, return the default
- if name not in self.arguments:
- return default
- # otherwise return the value
- return self.arguments[name]
- # Gets the value of a parameter as a float or returns the default
- def getArgumentAsFloat(self, name: str, default: float = None) -> float:
- # try to parse as a float, otherwise return the default
- try:
- return float(self.getArgument(name, default))
- except:
- return default
- # Gets the value of a parameter as an integer or returns the default
- def getArgumentAsInt(self, name: str, default: int = None) -> int:
- # try to parse as a integer, otherwise return the default
- try:
- return int(self.getArgument(name, default))
- except:
- return default
- # Allows retrieving values from the given GCODE line
- @staticmethod
- def getDirectArgument(line: str, key: str, default: str = None) -> str:
- if key not in line or (";" in line and line.find(key) > line.find(";") and ";ChangeAtZ" not in key and ";LAYER:" not in key):
- return default
- # allows for string lengths larger than 1
- sub_part = line[line.find(key) + len(key):]
- if ";ChangeAtZ" in key:
- m = re.search("^[0-4]", sub_part)
- elif ";LAYER:" in key:
- m = re.search("^[+-]?[0-9]*", sub_part)
- else:
- # the minus at the beginning allows for negative values, e.g. for delta printers
- m = re.search(r"^[-]?[0-9]*\.?[0-9]*", sub_part)
- if m is None:
- return default
- try:
- return m.group(0)
- except:
- return default
- # Converts the command parameter to a int or returns the default
- @staticmethod
- def getDirectArgumentAsFloat(line: str, key: str, default: float = None) -> float:
- # get the value from the command
- value = GCodeCommand.getDirectArgument(line, key, default)
- # stop here if it's the default
- if value == default:
- return value
- try:
- return float(value)
- except:
- return default
- # Converts the command parameter to a int or returns the default
- @staticmethod
- def getDirectArgumentAsInt(line: str, key: str, default: int = None) -> int:
- # get the value from the command
- value = GCodeCommand.getDirectArgument(line, key, default)
- # stop here if it's the default
- if value == default:
- return value
- try:
- return int(value)
- except:
- return default
- # Parses the arguments of the command on demand, only once
- def parseArguments(self):
- # stop here if we don't have any remaining components
- if len(self.components) <= 1:
- return None
- # iterate and index all of our parameters, skip the first component as it's the command
- for i in range(1, len(self.components)):
- # get our component
- component = self.components[i]
- # get the first character of the parameter, which is the name
- component_name = component[0]
- # get the value of the parameter (the rest of the string
- component_value = None
- # get our value if we have one
- if len(component) > 1:
- component_value = component[1:]
- # index the argument
- self.arguments[component_name] = component_value
- # clear the components to we don't process again
- self.components = []
- # Easy function for replacing any GCODE parameter variable in a given GCODE command
- @staticmethod
- def replaceDirectArgument(line: str, key: str, value: str) -> str:
- return re.sub(r"(^|\s)" + key + r"[\d\.]+(\s|$)", r"\1" + key + str(value) + r"\2", line)
- # Resets the model back to defaults
- def reset(self):
- self.command = None
- self.arguments = {}
- # The primary ChangeAtZ class that does all the gcode editing. This was broken out into an
- # independent class so it could be debugged using a standard IDE
- class ChangeAtZProcessor:
- # Holds our current height
- currentZ = None
- # Holds our current layer number
- currentLayer = None
- # Indicates if we're only supposed to apply our settings to a single layer or multiple layers
- applyToSingleLayer = False
- # Indicates if this should emit the changes as they happen to the LCD
- displayChangesToLcd = False
- # Indicates that this mod is still enabled (or not)
- enabled = True
- # Indicates if we're processing inside the target layer or not
- insideTargetLayer = False
- # Indicates if we have restored the previous values from before we started our pass
- lastValuesRestored = False
- # Indicates if the user has opted for linear move retractions or firmware retractions
- linearRetraction = True
- # Indicates if we're targeting by layer or height value
- targetByLayer = True
- # Indicates if we have injected our changed values for the given layer yet
- targetValuesInjected = False
- # Holds the last extrusion value, used with detecting when a retraction is made
- lastE = None
- # An index of our gcodes which we're monitoring
- lastValues = {}
- # The detected layer height from the gcode
- layerHeight = None
- # The target layer
- targetLayer = None
- # Holds the values the user has requested to change
- targetValues = {}
- # The target height in mm
- targetZ = None
- # Used to track if we've been inside our target layer yet
- wasInsideTargetLayer = False
- # boots up the class with defaults
- def __init__(self):
- self.reset()
- # Modifies the given GCODE and injects the commands at the various targets
- def execute(self, data):
- # short cut the whole thing if we're not enabled
- if not self.enabled:
- return data
- # our layer cursor
- index = 0
- for active_layer in data:
- # will hold our updated gcode
- modified_gcode = ""
- # mark all the defaults for deletion
- active_layer = self.markChangesForDeletion(active_layer)
- # break apart the layer into commands
- lines = active_layer.split("\n")
- # evaluate each command individually
- for line in lines:
- # trim or command
- line = line.strip()
- # skip empty lines
- if len(line) == 0:
- continue
- # update our layer number if applicable
- self.processLayerNumber(line)
- # update our layer height if applicable
- self.processLayerHeight(line)
- # check if we're at the target layer or not
- self.processTargetLayer()
- # process any changes to the gcode
- modified_gcode += self.processLine(line)
- # remove any marked defaults
- modified_gcode = self.removeMarkedChanges(modified_gcode)
- # append our modified line
- data[index] = modified_gcode
- index += 1
- # return our modified gcode
- return data
- # Builds the restored layer settings based on the previous settings and returns the relevant GCODE lines
- def getChangedLastValues(self) -> Dict[str, any]:
- # capture the values that we've changed
- changed = {}
- # for each of our target values, get the value to restore
- # no point in restoring values we haven't changed
- for key in self.targetValues:
- # skip target values we can't restore
- if key not in self.lastValues:
- continue
- # save into our changed
- changed[key] = self.lastValues[key]
- # return our collection of changed values
- return changed
- # Builds the relevant display feedback for each of the values
- def getDisplayChangesFromValues(self, values: Dict[str, any]) -> str:
- # stop here if we're not outputting data
- if not self.displayChangesToLcd:
- return ""
- # will hold all the default settings for the target layer
- codes = []
- # looking for wait for bed temp
- if "bedTemp" in values:
- codes.append("BedTemp: " + str(round(values["bedTemp"])))
- # looking for wait for Build Volume Temperature
- if "buildVolumeTemperature" in values:
- codes.append("buildVolumeTemperature: " + str(round(values["buildVolumeTemperature"])))
- # set our extruder one temp (if specified)
- if "extruderOne" in values:
- codes.append("Extruder 1 Temp: " + str(round(values["extruderOne"])))
- # set our extruder two temp (if specified)
- if "extruderTwo" in values:
- codes.append("Extruder 2 Temp: " + str(round(values["extruderTwo"])))
- # set global flow rate
- if "flowrate" in values:
- codes.append("Extruder A Flow Rate: " + str(values["flowrate"]))
- # set extruder 0 flow rate
- if "flowrateOne" in values:
- codes.append("Extruder 1 Flow Rate: " + str(values["flowrateOne"]))
- # set second extruder flow rate
- if "flowrateTwo" in values:
- codes.append("Extruder 2 Flow Rate: " + str(values["flowrateTwo"]))
- # set our fan speed
- if "fanSpeed" in values:
- codes.append("Fan Speed: " + str(values["fanSpeed"]))
- # set feedrate percentage
- if "speed" in values:
- codes.append("Print Speed: " + str(values["speed"]))
- # set print rate percentage
- if "printspeed" in values:
- codes.append("Linear Print Speed: " + str(values["printspeed"]))
- # set retract rate
- if "retractfeedrate" in values:
- codes.append("Retract Feed Rate: " + str(values["retractfeedrate"]))
- # set retract length
- if "retractlength" in values:
- codes.append("Retract Length: " + str(values["retractlength"]))
- # stop here if there's nothing to output
- if len(codes) == 0:
- return ""
- # output our command to display the data
- return "M117 " + ", ".join(codes) + "\n"
- # Converts the last values to something that can be output on the LCD
- def getLastDisplayValues(self) -> str:
- # convert our last values to something we can output
- return self.getDisplayChangesFromValues(self.getChangedLastValues())
- # Converts the target values to something that can be output on the LCD
- def getTargetDisplayValues(self) -> str:
- # convert our target values to something we can output
- return self.getDisplayChangesFromValues(self.targetValues)
- # Builds the the relevant GCODE lines from the given collection of values
- def getCodeFromValues(self, values: Dict[str, any]) -> str:
- # will hold all the desired settings for the target layer
- codes = self.getCodeLinesFromValues(values)
- # stop here if there are no values that require changing
- if len(codes) == 0:
- return ""
- # return our default block for this layer
- return ";[CAZD:\n" + "\n".join(codes) + "\n;:CAZD]"
- # Builds the relevant GCODE lines from the given collection of values
- def getCodeLinesFromValues(self, values: Dict[str, any]) -> List[str]:
- # will hold all the default settings for the target layer
- codes = []
- # looking for wait for bed temp
- if "bedTemp" in values:
- codes.append("M140 S" + str(values["bedTemp"]))
- # looking for wait for Build Volume Temperature
- if "buildVolumeTemperature" in values:
- codes.append("M141 S" + str(values["buildVolumeTemperature"]))
- # 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"]) + "")
- # set print rate percentage
- if "printspeed" in values:
- codes.append(";PRINTSPEED " + str(values["printspeed"]) + "")
- # set retract rate
- if "retractfeedrate" in values:
- if self.linearRetraction:
- 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.linearRetraction:
- 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.insideTargetLayer and self.wasInsideTargetLayer and not self.lastValuesRestored:
- # mark that we've injected the last values
- self.lastValuesRestored = 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.insideTargetLayer and not self.targetValuesInjected:
- # mark that we've injected the target values
- self.targetValuesInjected = True
- # inject the defaults
- return self.getTargetValues() + "\n" + self.getTargetDisplayValues()
- # nothing to do
- return ""
- # Returns the unmodified GCODE line from previous ChangeAtZ edits
- @staticmethod
- def getOriginalLine(line: str) -> str:
- # get the change at z original (cazo) details
- original_line = re.search(r"\[CAZO:(.*?):CAZO\]", line)
- # if we didn't get a hit, this is the original line
- if original_line is None:
- return line
- return original_line.group(1)
- # Builds the target layer settings based on the specified values and returns the relevant GCODE lines
- def getTargetValues(self) -> str:
- # build the gcode to change our current values
- return self.getCodeFromValues(self.targetValues)
- # Determines if the current line is at or below the target required to start modifying
- def isTargetLayerOrHeight(self) -> bool:
- # target selected by layer no.
- if self.targetByLayer:
- # 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.applyToSingleLayer:
- 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.applyToSingleLayer:
- return self.currentZ == self.targetZ
- else:
- return self.currentZ >= self.targetZ
- # Marks any current ChangeAtZ layer defaults in the layer for deletion
- @staticmethod
- def markChangesForDeletion(layer: str):
- return re.sub(r";\[CAZD:", ";[CAZD:DELETE:", layer)
- # Grabs the current height
- def processLayerHeight(self, line: str):
- # stop here if we haven't entered a layer yet
- if self.currentLayer is None:
- return
- # get our gcode command
- command = GCodeCommand.getFromLine(line)
- # skip if it's not a command we're interested in
- if command is None:
- return
- # stop here if this isn't a linear move command
- if command.command != "G0" and command.command != "G1":
- return
- # get our value from the command
- current_z = command.getArgumentAsFloat("Z", None)
- # stop here if we don't have a Z value defined, we can't get the height from this command
- if current_z is None:
- return
- # stop if there's no change
- if current_z == self.currentZ:
- return
- # set our current Z value
- self.currentZ = current_z
- # if we don't have a layer height yet, set it based on the current Z value
- if self.layerHeight is None:
- self.layerHeight = self.currentZ
- # Grabs the current layer number
- def processLayerNumber(self, line: str):
- # if this isn't a layer comment, stop here, nothing to update
- if ";LAYER:" not in line:
- return
- # get our current layer number
- current_layer = GCodeCommand.getDirectArgumentAsInt(line, ";LAYER:", None)
- # this should never happen, but if our layer number hasn't changed, stop here
- if current_layer == self.currentLayer:
- return
- # update our current layer
- self.currentLayer = current_layer
- # Makes any linear move changes and also injects either target or restored values depending on the plugin state
- def processLine(self, line: str) -> str:
- # used to change the given line of code
- modified_gcode = ""
- # track any values that we may be interested in
- self.trackChangeableValues(line)
- # if we're not inside the target layer, simply read the any
- # settings we can and revert any ChangeAtZ deletions
- if not self.insideTargetLayer:
- # 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.insideTargetLayer:
- modified_gcode += self.processLinearMove(line) + "\n"
- else:
- modified_gcode += line + "\n"
- # if we're targeting by layer we want to add our values just after the layer label
- if ";LAYER:" in line:
- modified_gcode += self.getInjectCode()
- # return our changed code
- return modified_gcode
- # Handles any linear moves in the current line
- def processLinearMove(self, line: str) -> str:
- # if it's not a linear motion command we're not interested
- if not ("G1 " in line or "G0 " in line):
- return line
- # always get our original line, otherwise the effect will be cumulative
- line = self.getOriginalLine(line)
- # get our command from the line
- linear_command = GCodeCommand.getLinearMoveCommand(line)
- # if it's not a linear move, we don't care
- if linear_command is None:
- return line
- # get our linear move parameters
- feed_rate = linear_command.arguments["F"]
- x_coord = linear_command.arguments["X"]
- y_coord = linear_command.arguments["Y"]
- z_coord = linear_command.arguments["Z"]
- extrude_length = linear_command.arguments["E"]
- # set our new line to our old line
- new_line = line
- # handle retract length
- new_line = self.processRetractLength(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
- # handle retract feed rate
- new_line = self.processRetractFeedRate(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
- # handle print speed adjustments
- if extrude_length is not None: # Only for extrusion moves.
- new_line = self.processPrintSpeed(feed_rate, new_line)
- # set our current extrude position
- self.lastE = extrude_length if extrude_length is not None else self.lastE
- # if no changes have been made, stop here
- if new_line == line:
- return line
- # return our updated command
- return self.setOriginalLine(new_line, line)
- # Handles any changes to print speed for the given linear motion command
- def processPrintSpeed(self, feed_rate: float, new_line: str) -> str:
- # if we're not setting print speed or we don't have a feed rate, stop here
- if "printspeed" not in self.targetValues or feed_rate is None:
- return new_line
- # get our requested print speed
- print_speed = int(self.targetValues["printspeed"])
- # if they requested no change to print speed (ie: 100%), stop here
- if print_speed == 100:
- return new_line
- # get our feed rate from the command
- feed_rate = GCodeCommand.getDirectArgumentAsFloat(new_line, "F") * (float(print_speed) / 100.0)
- # change our feed rate
- return GCodeCommand.replaceDirectArgument(new_line, "F", feed_rate)
- # Handles any changes to retraction length for the given linear motion command
- def processRetractLength(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
- # if we don't have a retract length in the file we can't add one
- if "retractlength" not in self.lastValues or self.lastValues["retractlength"] == 0:
- return new_line
- # if we're not changing retraction length, stop here
- if "retractlength" not in self.targetValues:
- return new_line
- # retractions are only F (feed rate) and E (extrude), at least in cura
- if x_coord is not None or y_coord is not None or z_coord is not None:
- return new_line
- # since retractions require both F and E, and we don't have either, we can't process
- if feed_rate is None or extrude_length is None:
- return new_line
- # stop here if we don't know our last extrude value
- if self.lastE is None:
- return new_line
- # if there's no change in extrude we have nothing to change
- if self.lastE == extrude_length:
- return new_line
- # if our last extrude was lower than our current, we're restoring, so skip
- if self.lastE < extrude_length:
- return new_line
- # get our desired retract length
- retract_length = float(self.targetValues["retractlength"])
- # subtract the difference between the default and the desired
- extrude_length -= (retract_length - self.lastValues["retractlength"])
- # replace our extrude amount
- return GCodeCommand.replaceDirectArgument(new_line, "E", extrude_length)
- # Used for picking out the retract length set by Cura
- def processRetractLengthSetting(self, line: str):
- # skip if we're not doing linear retractions
- if not self.linearRetraction:
- return
- # get our command from the line
- linear_command = GCodeCommand.getLinearMoveCommand(line)
- # if it's not a linear move, we don't care
- if linear_command is None:
- return
- # get our linear move parameters
- feed_rate = linear_command.arguments["F"]
- x_coord = linear_command.arguments["X"]
- y_coord = linear_command.arguments["Y"]
- z_coord = linear_command.arguments["Z"]
- extrude_length = linear_command.arguments["E"]
- # the command we're looking for only has extrude and feed rate
- if x_coord is not None or y_coord is not None or z_coord is not None:
- return
- # if either extrude or feed is missing we're likely looking at the wrong command
- if extrude_length is None or feed_rate is None:
- return
- # cura stores the retract length as a negative E just before it starts printing
- extrude_length = extrude_length * -1
- # if it's a negative extrude after being inverted, it's not our retract length
- if extrude_length < 0:
- return
- # what ever the last negative retract length is it wins
- self.lastValues["retractlength"] = extrude_length
- # Handles any changes to retraction feed rate for the given linear motion command
- def processRetractFeedRate(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
- # skip if we're not doing linear retractions
- if not self.linearRetraction:
- return new_line
- # if we're not changing retraction length, stop here
- if "retractfeedrate" not in self.targetValues:
- return new_line
- # retractions are only F (feed rate) and E (extrude), at least in cura
- if x_coord is not None or y_coord is not None or z_coord is not None:
- return new_line
- # since retractions require both F and E, and we don't have either, we can't process
- if feed_rate is None or extrude_length is None:
- return new_line
- # get our desired retract feed rate
- retract_feed_rate = float(self.targetValues["retractfeedrate"])
- # convert to units/min
- retract_feed_rate *= 60
- # replace our feed rate
- return GCodeCommand.replaceDirectArgument(new_line, "F", retract_feed_rate)
- # Used for finding settings in the print file before we process anything else
- def processSetting(self, line: str):
- # if we're in layers already we're out of settings
- if self.currentLayer is not None:
- return
- # check our retract length
- self.processRetractLengthSetting(line)
- # Sets the flags if we're at the target layer or not
- def processTargetLayer(self):
- # skip this line if we're not there yet
- if not self.isTargetLayerOrHeight():
- # flag that we're outside our target layer
- self.insideTargetLayer = 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.insideTargetLayer = True
- # Removes all the ChangeAtZ layer defaults from the given layer
- @staticmethod
- def removeMarkedChanges(layer: str) -> str:
- return re.sub(r";\[CAZD:DELETE:[\s\S]+?:CAZD\](\n|$)", "", layer)
- # Resets the class contents to defaults
- def reset(self):
- self.targetValues = {}
- self.applyToSingleLayer = False
- self.lastE = None
- self.currentZ = None
- self.currentLayer = None
- self.targetByLayer = True
- self.targetLayer = None
- self.targetZ = None
- self.layerHeight = None
- self.lastValues = {"speed": 100}
- self.linearRetraction = True
- self.insideTargetLayer = False
- self.targetValuesInjected = False
- self.lastValuesRestored = False
- self.wasInsideTargetLayer = False
- self.enabled = True
- # Sets the original GCODE line in a given GCODE command
- @staticmethod
- def setOriginalLine(line, original) -> str:
- return line + ";[CAZO:" + original + ":CAZO]"
- # Tracks the change in gcode values we're interested in
- def trackChangeableValues(self, line: str):
- # simulate a print speed command
- if ";PRINTSPEED" in line:
- line = line.replace(";PRINTSPEED ", "M220 S")
- # simulate a retract feedrate command
- if ";RETRACTFEEDRATE" in line:
- line = line.replace(";RETRACTFEEDRATE ", "M207 F")
- # simulate a retract length command
- if ";RETRACTLENGTH" in line:
- line = line.replace(";RETRACTLENGTH ", "M207 S")
- # get our gcode command
- command = GCodeCommand.getFromLine(line)
- # stop here if it isn't a G or M command
- if command is None:
- return
- # handle retract length changes
- if command.command == "M207":
- # get our retract length if provided
- if "S" in command.arguments:
- self.lastValues["retractlength"] = command.getArgumentAsFloat("S")
- # get our retract feedrate if provided, convert from mm/m to mm/s
- if "F" in command.arguments:
- self.lastValues["retractfeedrate"] = command.getArgumentAsFloat("F") / 60.0
- # move to the next command
- return
- # handle bed temp changes
- if command.command == "M140" or command.command == "M190":
- # get our bed temp if provided
- if "S" in command.arguments:
- self.lastValues["bedTemp"] = command.getArgumentAsFloat("S")
- # move to the next command
- return
- # handle Build Volume Temperature changes, really shouldn't want to wait for enclousure temp mid print though.
- if command.command == "M141" or command.command == "M191":
- # get our bed temp if provided
- if "S" in command.arguments:
- self.lastValues["buildVolumeTemperature"] = command.getArgumentAsFloat("S")
- # move to the next command
- return
- # handle extruder temp changes
- if command.command == "M104" or command.command == "M109":
- # get our temperature
- temperature = command.getArgumentAsFloat("S")
- # don't bother if we don't have a temperature
- if temperature 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"] = temperature
- if extruder is None or extruder == 1:
- self.lastValues["extruderTwo"] = temperature
- # 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
- temperature = command.getArgumentAsFloat("S")
- # don't bother if we don't have a flow rate (for some reason)
- if temperature 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"] = temperature
- elif extruder == 1:
- self.lastValues["flowrateOne"] = temperature
- elif extruder == 1:
- self.lastValues["flowrateTwo"] = temperature
- # 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
|