I wanted a macro for heating the bed of my printer and allowing the bed, chamber, and frame to come up to temp for a while. My first solution worked, but M190 and G4 are both blocking commands: it was impossible to interrupt the macro without using the emergency stop. And sometimes I wanted to be able to move the toolhead or do minor maintenance while the printer is warming up. This is the first macro I used:
[gcode_macro HEAT_SOAK]
description: heats the bed for a while
gcode:
{% set TARGET = params.TARGET | default(0) %}
{% set PERIOD = (params.PERIOD | default(60) | int) %} ## minutes
;; put the bed and nozzle where they're a safe distance apart
G28
CENTER
M84 ;; turn off steppers
;; run the fan to circulate air
_SET_FAN_SPEED PERCENT=50
M190 S{ TARGET } ; wait for bed temp
RESPOND TYPE=command MSG="will soak for { PERIOD }m"
G4 P{ PERIOD * 1000 * 60 }
After noodling on it for a while I realized I could do a timer-based macro with delayed_gcode that would keep the heater on and the printer from becoming idle while still allowing me to cancel it cleanly or to do other stuff like move the head around. This is that solution:
[gcode_macro HEAT_SOAK]
description: heats the bed for a while
variable_target_temp: 0
variable_stage: None ## heating -> soaking -> done -> None
## in seconds
variable_check_interval: 10
variable_soak_time_remaining: 0
variable_total_time_elapsed: 0
gcode:
{% set TARGET = params.TARGET | default(0) | float %}
{% set DURATION = (params.DURATION | default(5) | int) * 60 %} ## minutes to seconds
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_temp VALUE={ TARGET }
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'heating'"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={ DURATION }
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE=0
;; fire up the heater
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={ TARGET }
;; run the fan to circulate air
_SET_FAN_SPEED PERCENT=50
;; put the bed and nozzle where they're a safe distance apart
G28
CENTER
M84 ;; turn off steppers
UPDATE_DELAYED_GCODE ID=heat_soaker DURATION={ check_interval }
[gcode_macro CANCEL_HEAT_SOAK]
description: cancels an in-progress HEAT_SOAK cycle
gcode:
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'cancel'"
UPDATE_DELAYED_GCODE ID=heat_soaker DURATION=1
[delayed_gcode heat_soaker]
; ## debug
; { action_respond_info( printer['gcode_macro HEAT_SOAK'] | tojson )}
gcode:
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
## update total time elapsed
{% set total_time_elapsed = heat_soak.total_time_elapsed + heat_soak.check_interval %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
{% set stage = heat_soak.stage %}
{% if stage == "heating" and printer.heater_bed.temperature >= heat_soak.target_temp %}
{% set stage = "soaking" %}
{% endif %}
{% if stage == "soaking" %}
## update soak countdown
{% set soak_time_remaining = [heat_soak.soak_time_remaining - heat_soak.check_interval, 0] | max %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={ soak_time_remaining }
{% if soak_time_remaining == 0 %}
{% set stage = "done" %}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{ stage }'"
{% if stage in ("done", "cancel") %}
{% if stage == "cancel" %}
{% set stage = "done" %}
TURN_OFF_HEATERS
M107 ; turn off fan
M117 { "soak cancelled after ~%.1fm" | format(total_time_elapsed / 60.0) }
{% else %}
M117 { "soak complete after %.1fm" | format(total_time_elapsed / 60.0) }
{% endif %}
## reset all state vars, except stage, which may be queried via the api
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_temp VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE=0
{% else %}
{% if total_time_elapsed % 90 == 0 %}
## output status periodically
{% if stage == "heating" %}
M117 { "heating -- %.1fm elapsed" | format(total_time_elapsed / 60.0) }
{% elif stage == "soaking" %}
M117 { "soaking -- %.1fm remaining" | format(soak_time_remaining / 60.0) }
{% endif %}
{% endif %}
## trigger ourselves again
UPDATE_DELAYED_GCODE ID=heat_soaker DURATION={ heat_soak.check_interval }
## dwell for 1ms to prevent from going idle
G4 P1
{% endif %}
There are actually 3 macros in here:
HEAT_SOAK ā turns on the heater and starts a timer that calls itself until the soak duration has elapsed
CANCEL_HEAT_SOAK ā causes the timer to cancel the soak process and turn off the heater
heat_soaker (delayed g-code macro) ā called periodically (every 10s) to update state and prevent the idle timeout from triggering.
Itās worth noting that after the heat soak process completes, the heater is left on. The idle timeout will kick in and shut everything down later on. This gives me time to start a print after soakingās finished, without things cooling down too much. CANCEL_HEAT_SOAK causes the heater to be turned off immediately.
The implementation feels a bit clunky but overall Iām happy with it, I think. I welcome any suggestions or critiques.
A fun bonus: this will also allow me to use Home Assistant to send me a notification when the soak process is complete! Moonraker allows you to query a gcode_macro and return its variables. The stage variable will be what I trigger my automation on.
Wow, it seems you have a real good grasp of macroās. I am impressed. I am a newbie on klipper and just finished the construction of my first printer (a 350mm Voron 2.4) so I am still tinkering with settings and have a lot to learn. Thanks for providing the macroās. They are a good example and I will probably use them. As a retired programmer I know that for any task there are a million ways to accomplish it. Klunky isnāt a bad thing as long as it works.
(I know itās an old topic but just found it on google)
This looks awesome!! Thanks for sharing this
Iām trying to use it and missing some other macros you call from within this one. i.e.:
Unknown command:"_SET_FAN_SPEED"
Unknown command:āCENTERā
Is there any github you have where you have all your macros and configurations by any chance?
Thanks for the macro. It is working well for me (standalone), and Iāve been able modify it to use my chamber sensor for the target.
What I havenāt been able to figure out is how to use this as part of my PRINT_START macro. It seems like HEAT_SOAK doesnāt block, so PRINT_START continues while āheat_soakerā is still being ran. Reading your comment, it sounds like you are using using your notification to let you know when you can start your print, so itās different than what Iām trying.
Iām glad itās useful! Yeah, I only use it as a standalone process. I donāt think thereās any way to ājoinā to a background task in Klipper, although thatād be useful!
So the way I use this is to start the heat soak ā say 15m at 110Ā°C ā when Iām getting ready to start a job. When I get the notification itās done, then I actually start the job. If Iām doing multiple small prints I only need to soak once.
Add a chamber thermistor, add the macro call in START_PRINT/PRINT_START, add HEAT_SOAK and TEMPERATURE_WAIT. Done? It wonāt wait for the specified time but it will wait for the chamber to reach the temperature you want.
If you want to use this during a print start script and wait for the heat soak, here are the changes I made:
Put a PAUSE after the safe distance apart code in the HEAT_SOAK macro
Put a CANCEL_PRINT in the if stage == "cancel" condition
Put a RESUME in the else case for the above cancel condition check
Optional: If you have additional gcode to run after the heat soak completes and before the print resumes, add a SOAK_COMPLETE call right before RESUME and create the SOAK_COMPLETE macro to do whatever else you need to do
Note that if you have customized PAUSE, āCANCEL_PRINTā, or RESUME macros, and you used rename_existing, you can call the original instead of the customized macros if you desire. Here is my alteration of the HEAT_SOAK macro:
[gcode_macro HEAT_SOAK]
description: heats the bed for a while
variable_target_temp: 0
variable_stage: None ## heating -> soaking -> done -> None
## in seconds
variable_check_interval: 10
variable_soak_time_remaining: 0
variable_total_time_elapsed: 0
gcode:
{% set TARGET = params.TARGET | default(0) | float %}
{% set DURATION = (params.DURATION | default(15) | int) * 60 %} ## minutes to seconds
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_temp VALUE={ TARGET }
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'heating'"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={ DURATION }
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE=0
;; fire up the heater
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={ TARGET }
;; run the fan to circulate air
_SET_FAN_SPEED PERCENT=50
;; put the bed and nozzle where they're a safe distance apart
G28
CENTER
M84 ;; turn off steppers
UPDATE_DELAYED_GCODE ID=heat_soaker DURATION={ check_interval }
;; pause the print during heat soak
BASE_PAUSE
[gcode_macro CANCEL_HEAT_SOAK]
description: cancels an in-progress HEAT_SOAK cycle
gcode:
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'cancel'"
UPDATE_DELAYED_GCODE ID=heat_soaker DURATION=1
[delayed_gcode heat_soaker]
; ## debug
; { action_respond_info( printer['gcode_macro HEAT_SOAK'] | tojson )}
gcode:
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
## update total time elapsed
{% set total_time_elapsed = heat_soak.total_time_elapsed + heat_soak.check_interval %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
{% set stage = heat_soak.stage %}
{% if stage == "heating" and printer.heater_bed.temperature >= heat_soak.target_temp %}
{% set stage = "soaking" %}
{% endif %}
{% if stage == "soaking" %}
## update soak countdown
{% set soak_time_remaining = [heat_soak.soak_time_remaining - heat_soak.check_interval, 0] | max %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={ soak_time_remaining }
{% if soak_time_remaining == 0 %}
{% set stage = "done" %}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{ stage }'"
{% if stage in ("done", "cancel") %}
{% if stage == "cancel" %}
{% set stage = "done" %}
TURN_OFF_HEATERS
M107 ; turn off fan
M117 { "soak cancelled after ~%.1fm" | format(total_time_elapsed / 60.0) }
;; heat soak was cancelled -- also cancel the print
CANCEL_PRINT
{% else %}
M117 { "soak complete after %.1fm" | format(total_time_elapsed / 60.0) }
;; heat soak is complete. Call the complete macro prior to starting the print
SOAK_COMPLETE
;; resume / start the print
BASE_RESUME
{% endif %}
## reset all state vars, except stage, which may be queried via the api
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_temp VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE=0
{% else %}
{% if total_time_elapsed % 90 == 0 %}
## output status periodically
{% if stage == "heating" %}
M117 { "heating -- %.1fm elapsed" | format(total_time_elapsed / 60.0) }
{% elif stage == "soaking" %}
M117 { "soaking -- %.1fm remaining" | format(soak_time_remaining / 60.0) }
{% endif %}
{% endif %}
## trigger ourselves again
UPDATE_DELAYED_GCODE ID=heat_soaker DURATION={ heat_soak.check_interval }
## dwell for 1ms to prevent from going idle
G4 P1
{% endif %}
hello, just built a voron 2.4r2, iām having problems with parts staying stuck when initially printing, so I thought heat soak would be a good idea. I ran across your post. I added the macro to my printer.cfg.
I run heat_soak, my toolhead homes and moves to the center, but the heater_bed never turn on.
Iām assuming I need to add the temperature I want to the variable_target_temp: 0. I set it to 110, but it does not turn on, any ideas?
My version of the script is here: klipper-voron2.4-config/printer_data/config/heatsoak.cfg at mainline Ā· garethky/klipper-voron2.4-config Ā· GitHub
It uses a second temperature sensor and stops the heat soak when the rate of temperature change on that sensor drops below a target. I have the default set to a rate of change of less than 0.75 degrees C per minute. This means that if the bed is already hot the soak time will be shorter. You call it like this:
You also have to split your print start script where you call HEAT_SOAK. The call to PAUSE will not stop the print start script from running, it just stops the next line of GCode in the file being printed from running. So now I have:
PRINT_PREPARE [...] ; ends with a call to HEAT_SOAK
PRINT_START [...] ; does mesh bed leveling after heat soak
The only real gap now is that we cant configure a smooth_time on temperature sensors so the data we get from the sensor is noisy. I might do something about that but honestly this is working fine for my needs.
Ok I had yet another bug with that action_respond_info statement that I fixed. No more format() calls in the code now.
I implemented a callback in heat_soak. My print_start passes a callback to heat_soak. heat_soak then calls that macro when it reaches the done state. This means the 2 parts of the print start process are chained together and I only have to call print_start in my slicer.
I also added a callback to print_start itself, so it can start running a test print macro after the entire startup process finishes. Running the pressure advance test macro after print start looks like this:
The -%} and {%- are how you remove whitespace around the value, so the result is 1 line, this is required for this trick to work.
I didnāt find a way to nest the callbacks, with the way jinja and klipper handle string escaping I donāt see how to keep the quotes in the strings intact. I can think of more devious ways of achieving this but you probably wont like them.
Programming macros with callbacks destroys the intended simplicity of the language. Some sort of barrier statement that blocks further evaluation of a macro is probably a better solution. Like the await keyword.
Hy @garethky , I made some changes in your macro. The most important are:
mintemp variable: my chamber temp sensor takes too long to react upon the bed heating, so I need to define a minimum temperature to finish the heatsoak, otherwise it finishes in the beginning;
check_interval: defined two check_interval, one for heating stage, which is fast and need more frequent update and another to soak stage, where longer updates helps with delta temperature calculations.
The MIN_TEMP idea makes sense. I could also see calling the macro with a formula, like MIN_TEMP=(TARGET_TEMP * 0.85) so it scales with the target temp. I can also make it so that its optional, so if you donāt provide a value it is not checked.
Would smoothing the temperature reading have made a difference? Iāve been thinking about making the interval 1 second, gathering a reading in an array and then computing an average over 5 seconds. Basically doing the smoothing in the macro. Iām not exactly sure there are enough array operators to do this in the language though, you would ideally want something like push() and pop() or slice().
This would uncouple the interval that the temp gets polled and the interval that the messages are posted, so that would add the same kind of interval variables that you added.