Skip to content

Instantly share code, notes, and snippets.

@cdracars
Created September 26, 2025 00:58
Show Gist options
  • Select an option

  • Save cdracars/0d343ae438029f93f1d0f2c69857a514 to your computer and use it in GitHub Desktop.

Select an option

Save cdracars/0d343ae438029f93f1d0f2c69857a514 to your computer and use it in GitHub Desktop.
# ============================================================================
# KLIPPER HEAT SOAK MACRO (SMART: TIME OR CHAMBER TEMP)
# ============================================================================
# Author: Cody Dracars
# Date: 2025-04-05 (smart mode added 2025-09-25)
# License: MIT
#
# Heats bed to target and performs a soak using either:
# - Time-based countdown, or
# - Chamber-temperature target with hold requirement (if a chamber sensor exists)
#
# Features
# - Configurable bed temp, soak duration, fan speed
# - Auto-detects chamber sensor (temperature_sensor/heater_generic/temperature_fan)
# - MODE switch: time | chamber | auto (default auto)
# - Chamber hold timer to ensure temperature stability
# - 1s tick loop via delayed_gcode (robust against long G4 timing issues)
# - Gentle toolhead motion for air circulation
# - Progress updates (M117/M118) and optional beeps on completion
# - Cancel macro: HEAT_SOAK_CANCEL
#
# Usage Examples
# HEAT_SOAK ; default time mode if no chamber target
# HEAT_SOAK BED_TEMP=110 SOAK_TIME=1800 ; 30 min soak (time)
# HEAT_SOAK MODE=chamber CHAMBER_TEMP=45 ; wait for chamber 45°C + hold
# HEAT_SOAK CHAMBER_TEMP=45 ; auto: uses chamber if found, else time
# HEAT_SOAK CHAMBER_TEMP=45 SENSOR="heater_generic chamber" ; explicit sensor
# HEAT_SOAK BED_TEMP=110 FAN_SPEED=192 ; ~75% fan
#
# Installation
# - Save as heat_soak_smart.cfg and [include heat_soak_smart.cfg] in printer.cfg
# - FIRMWARE_RESTART
# - Optional beeper (see bottom); macro auto-detects BEEP
#
# ============================================================================
[gcode_macro HEAT_SOAK]
description: Heat bed and soak chamber (time or chamber sensor)
# Tunables with sane defaults
variable_mode: 0 # 0=time, 1=chamber
variable_target_bed: 100
variable_target_chamber: 0
variable_soak_time: 900
variable_fan_speed: 128
variable_update_interval: 30 # seconds between UI updates/motions
variable_move_distance: 10 # mm sweep distance (X)
variable_elapsed: 0
variable_hold: 0
variable_hold_required: 120 # seconds at/above chamber target
variable_timeout: 3600 # max seconds to wait for chamber
variable_sensor_key: ""
variable_running: 0
gcode:
{% set bed_temp = params.BED_TEMP|default(100)|int %}
{% set soak_time = params.SOAK_TIME|default(900)|int %}
{% set fan_speed = params.FAN_SPEED|default(128)|int %}
{% set chamber_temp = params.CHAMBER_TEMP|default(0)|int %}
{% set timeout = params.CHAMBER_TIMEOUT|default(3600)|int %}
{% set hold_req = params.CHAMBER_HOLD|default(120)|int %}
{% set req_mode = params.MODE|default("auto")|lower %}
{% set sensor = params.SENSOR|default("") %}
{% if bed_temp < 0 or bed_temp > 150 %}{action_raise_error("BED_TEMP 0-150")}{% endif %}
{% if soak_time < 60 or soak_time > 7200 %}{action_raise_error("SOAK_TIME 60-7200s")}{% endif %}
{% if fan_speed < 0 or fan_speed > 255 %}{action_raise_error("FAN_SPEED 0-255")}{% endif %}
{% if chamber_temp < 0 or chamber_temp > 90 %}{action_raise_error("CHAMBER_TEMP 0-90")}{% endif %}
# Resolve chamber sensor
{% set ns = namespace(key=sensor) %}
{% if not ns.key or ns.key not in printer %}
{% for k in ["temperature_sensor chamber","temperature_sensor enclosure",
"heater_generic chamber","heater_generic enclosure",
"temperature_fan chamber","temperature_fan enclosure"] %}
{% if k in printer %}{% set ns.key = k %}{% break %}{% endif %}
{% endfor %}
{% endif %}
{% set has_chamber = ns.key and (ns.key in printer) %}
# Decide mode
{% if req_mode == "chamber" and not has_chamber %}
{action_raise_error("MODE=chamber requires a chamber SENSOR; pass SENSOR=... or define a chamber sensor")}
{% endif %}
{% if req_mode == "chamber" and chamber_temp <= 0 %}
{action_raise_error("MODE=chamber requires CHAMBER_TEMP>0")}
{% endif %}
{% set mode_num = 1 if (req_mode=="chamber" or (req_mode=="auto" and has_chamber and chamber_temp>0)) else 0 %}
M117 Heat Soak: Heating bed to {bed_temp}°C
M118 Heat Soak: Mode={("chamber" if mode_num==1 else "time")}; Sensor='{{ns.key|default("none")}}'
M118 Heat Soak: Targets → bed={bed_temp}°C; {("chamber="+chamber_temp|string+"°C" if mode_num==1 else "soak="+soak_time|string+"s")}
# Heat bed and wait
M190 S{bed_temp}
# Home if needed, center toolhead, set fan
{% if printer.toolhead.homed_axes != "xyz" %} G28 {% endif %}
G90
{% set cx = printer.toolhead.axis_maximum.x/2 %}
{% set cy = printer.toolhead.axis_maximum.y/2 %}
{% set safe_z = 10 %}
G1 X{cx} Y{cy} Z{safe_z} F6000
{% if fan_speed>0 %} M106 S{fan_speed} {% endif %}
# Seed state for ticker
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=mode VALUE={mode_num}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_bed VALUE={bed_temp}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_chamber VALUE={chamber_temp}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time VALUE={soak_time}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=fan_speed VALUE={fan_speed}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=update_interval VALUE={printer["gcode_macro HEAT_SOAK"].update_interval}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=move_distance VALUE={printer["gcode_macro HEAT_SOAK"].move_distance}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=hold_required VALUE={hold_req}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=timeout VALUE={timeout}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=sensor_key VALUE="'{{ ns.key|default("") }}'"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=elapsed VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=hold VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=running VALUE=1
M118 Heat Soak: Started. Use HEAT_SOAK_CANCEL to abort.
UPDATE_DELAYED_GCODE ID=_HEAT_SOAK_TICK DURATION=1
# Periodic tick driving countdown or chamber hold
[delayed_gcode _HEAT_SOAK_TICK]
initial_duration: 0
gcode:
{% set m = printer["gcode_macro HEAT_SOAK"] %}
{% if not m.running %}
UPDATE_DELAYED_GCODE ID=_HEAT_SOAK_TICK DURATION=0
{% endif %}
{% set e = m.elapsed|int %}
{% set iv = m.update_interval|int %}
{% set mode = m.mode|int %}
{% set cx = printer.toolhead.axis_maximum.x/2 %}
{% set move = m.move_distance|float %}
{% set ready = 0 %}
{% set chamber = None %}
{% if m.sensor_key and m.sensor_key in printer %}
{% set chamber = printer[m.sensor_key].temperature|float %}
{% endif %}
{% if mode==1 %}
{% set hold = m.hold|int %}
{% if chamber is not none and chamber >= m.target_chamber|float %}
{% set hold = hold + 1 %}
{% else %}
{% set hold = 0 %}
{% endif %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=hold VALUE={hold}
{% if hold >= m.hold_required|int or e >= m.timeout|int %}
{% set ready = 1 %}
{% endif %}
{% else %}
{% if e >= m.soak_time|int %}
{% set ready = 1 %}
{% endif %}
{% endif %}
# UI + gentle motion every iv seconds
{% if e % iv == 0 %}
{% if mode==1 %}
M117 Heat Soak: Chamber {chamber|default(-1)|round(1)} / {m.target_chamber}°C
M118 Heat Soak: chamber={chamber|default(-1)|round(1)}°C, hold={m.hold}/{m.hold_required}s
{% else %}
M117 Heat Soak: {m.soak_time - e}s left
M118 Heat Soak: elapsed={e}s / {m.soak_time}s
{% endif %}
{% set dir = -1 if ((e // iv) % 2 == 0) else 1 %}
{% set x = cx + dir * (move/2) %}
G1 X{x} F3000
{% endif %}
{% if ready %}
M107
G1 X{cx} Y{printer.toolhead.axis_minimum.y + 10} Z10 F6000
M117 Heat Soak: Complete!
M118 Heat Soak: Complete.
{% if 'gcode_macro BEEP' in printer %} BEEP ; G4 P300 ; BEEP ; G4 P600 ; BEEP {% endif %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=running VALUE=0
UPDATE_DELAYED_GCODE ID=_HEAT_SOAK_TICK DURATION=0
{% else %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=elapsed VALUE={e+1}
UPDATE_DELAYED_GCODE ID=_HEAT_SOAK_TICK DURATION=1
{% endif %}
[gcode_macro HEAT_SOAK_CANCEL]
gcode:
UPDATE_DELAYED_GCODE ID=_HEAT_SOAK_TICK DURATION=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=running VALUE=0
M107
M117 Heat Soak: Canceled
M118 Heat Soak: Canceled
# Optional beeper
# [output_pin beeper]
# pin: YOUR_BEEPER_PIN
# pwm: True
# cycle_time: 0.0005
#
# [gcode_macro BEEP]
# gcode:
# SET_PIN PIN=beeper VALUE=0.8
# G4 P100
# SET_PIN PIN=beeper VALUE=0
# Troubleshooting
# - Instant finish: Your config had G4 issues; this macro uses a 1s ticker.
# - Unknown command BEEP: define BEEP or remove beeps.
# - No fan: ensure [fan] is configured.
# - MODE=chamber errors: provide SENSOR=... and CHAMBER_TEMP>0, or use MODE=time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment