Show errors and warnings on the display (_ERROR, _WARN)

action_raise_error aborts while the macro renders, so an M117 in the same
macro never ran and errors such as the RESUME lane check only reached the
console. messages.cfg adds _ERROR (M117, then raise in a separate macro)
and _WARN (RESPOND TYPE=error + M117); all errors and warnings use them.
PAUSE shows AFC's error as the pause reason; RESUME, CANCEL_PRINT and
PRINT_WARMUP clear the display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-16 17:41:41 +02:00
1 parent a67e99015f
commit fbdb2d5e1d
11 files changed
+78 -29

No files matched your search

+6 -1
View File
@@ -64,7 +64,12 @@ detection, `filament_error_sensitivity`), iHeater chamber heater.
(remembered in the saved variable `nozzle_retract`); `PRINT_START` undoes (remembered in the saved variable `nozzle_retract`); `PRINT_START` undoes
it over the bucket with `_NOZZLE_UNRETRACT`. it over the bucket with `_NOZZLE_UNRETRACT`.
- Inside `gcode:` use `;` for comments on G-code lines; `#` lines are - Inside `gcode:` use `;` for comments on G-code lines; `#` lines are
stripped by the config parser. stripped by the config parser. Never put a comment on an `M117` line:
M117 shows the raw rest of the line, comment included.
- Errors and warnings go through `_ERROR` / `_WARN` (`macros/messages.cfg`),
not `action_raise_error` / `RESPOND TYPE=error`, so they also show on the
display. `action_raise_error` aborts while rendering, so an `M117` in the
same macro would never run. No double quotes in `MSG`/`DISPLAY`.
- After a refactor run `python tools/effective_config.py HEAD` and check that - After a refactor run `python tools/effective_config.py HEAD` and check that
only the intended sections changed. only the intended sections changed.
- Render-test macros with Jinja2 using Klipper's delimiters - Render-test macros with Jinja2 using Klipper's delimiters
+1
View File
@@ -16,6 +16,7 @@ everything below, grouped by topic. Comment out what you do not use there.
| File | Macros | Needs | | File | Macros | Needs |
|---|---|---| |---|---|---|
| `save_variables.cfg` | the one `[save_variables]` for all macros (file `bd_pa_variables.cfg` in the config folder) | — | | `save_variables.cfg` | the one `[save_variables]` for all macros (file `bd_pa_variables.cfg` in the config folder) | — |
| `messages.cfg` | `_ERROR`, `_WARN`: errors and warnings shown in the console and on the display (M117, the status line in Mainsail and KlipperScreen); PAUSE shows AFC's error, RESUME, CANCEL_PRINT and PRINT_WARMUP clear the display | — |
| **blobifier/** | | | | **blobifier/** | | |
| `blobifier.cfg` | `BLOBIFIER`, `BLOBIFIER_CLEAN`, `BLOBIFIER_PARK`, `BLOBIFIER_SHAKE_BUCKET`, ...: purge bucket and nozzle brush for AFC (Turtleblobifier, heavily modified) | Blobifier hardware, AFC | | `blobifier.cfg` | `BLOBIFIER`, `BLOBIFIER_CLEAN`, `BLOBIFIER_PARK`, `BLOBIFIER_SHAKE_BUCKET`, ...: purge bucket and nozzle brush for AFC (Turtleblobifier, heavily modified) | Blobifier hardware, AFC |
| `blobifier_hw.cfg` | servo and bucket switch pins | — | | `blobifier_hw.cfg` | servo and bucket switch pins | — |
+1 -1
View File
@@ -162,7 +162,7 @@ gcode:
{% set verdict = 'FAIL: ' ~ failed|join(', ') if failed else ('OK' if checked else 'NOTHING CHECKED') %} {% set verdict = 'FAIL: ' ~ failed|join(', ') if failed else ('OK' if checked else 'NOTHING CHECKED') %}
RESPOND MSG="PURGE_CHECK {lane}: {motion}; {press} -> {verdict}" RESPOND MSG="PURGE_CHECK {lane}: {motion}; {press} -> {verdict}"
{% if failed and cfg.mode == "enforce" %} {% if failed and cfg.mode == "enforce" %}
RESPOND TYPE=error MSG="PURGE_CHECK: {lane} {failed|join(', ')} - check extruder and lane" _WARN MSG="PURGE_CHECK: {lane} {failed|join(', ')} - check extruder and lane" DISPLAY="Purge check failed: {lane}, paused"
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=last_failed VALUE=True SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=last_failed VALUE=True
PAUSE ; no effect while already paused (resume purge): see _RESUME_PURGE_VERIFY PAUSE ; no effect while already paused (resume purge): see _RESUME_PURGE_VERIFY
{% endif %} {% endif %}
+7 -7
View File
@@ -49,13 +49,13 @@ gcode:
{% set min_temp = printer.configfile.settings.extruder.min_extrude_temp|float %} {% set min_temp = printer.configfile.settings.extruder.min_extrude_temp|float %}
{% set temp = params.TEMP|default([printer.extruder.target, 0]|max)|float %} {% set temp = params.TEMP|default([printer.extruder.target, 0]|max)|float %}
{% if 'bdpressure_check' not in printer %} {% if 'bdpressure_check' not in printer %}
{action_raise_error("PURGE_UNTIL_EMPTY: needs bdpressure_check.py and [bdpressure_check]")} _ERROR MSG="PURGE_UNTIL_EMPTY: needs bdpressure_check.py and [bdpressure_check]"
{% elif printer.print_stats.state == "printing" %} {% elif printer.print_stats.state == "printing" %}
{action_raise_error("PURGE_UNTIL_EMPTY: printer is printing, pause the print first")} _ERROR MSG="PURGE_UNTIL_EMPTY: printer is printing, pause the print first"
{% elif "xyz" not in printer.toolhead.homed_axes %} {% elif "xyz" not in printer.toolhead.homed_axes %}
{action_raise_error("PURGE_UNTIL_EMPTY: home the printer first")} _ERROR MSG="PURGE_UNTIL_EMPTY: home the printer first"
{% elif temp < min_temp %} {% elif temp < min_temp %}
{action_raise_error("PURGE_UNTIL_EMPTY: give the filament temperature, e.g. PURGE_UNTIL_EMPTY TEMP=240")} _ERROR MSG="PURGE_UNTIL_EMPTY: give the filament temperature, e.g. PURGE_UNTIL_EMPTY TEMP=240" DISPLAY="PURGE_UNTIL_EMPTY: give TEMP=..."
{% else %} {% else %}
BDP_CHECK_QUERY BDP_CHECK_QUERY
_PURGE_UNTIL_EMPTY_RUN MAX={max_purges} LENGTH={length} UNSYNC={unsync} TEMP={temp} _PURGE_UNTIL_EMPTY_RUN MAX={max_purges} LENGTH={length} UNSYNC={unsync} TEMP={temp}
@@ -65,7 +65,7 @@ gcode:
[gcode_macro _PURGE_UNTIL_EMPTY_RUN] [gcode_macro _PURGE_UNTIL_EMPTY_RUN]
gcode: gcode:
{% if not printer.bdpressure_check.available %} {% if not printer.bdpressure_check.available %}
{action_raise_error("PURGE_UNTIL_EMPTY: bd_pressure load-check firmware not found")} _ERROR MSG="PURGE_UNTIL_EMPTY: bd_pressure load-check firmware not found" DISPLAY="PURGE_UNTIL_EMPTY: no bd_pressure"
{% endif %} {% endif %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=0 SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=0
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE=0 SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE=0
@@ -105,7 +105,7 @@ gcode:
{% set p = printer.bdpressure_check %} {% set p = printer.bdpressure_check %}
{% set n = cfg.count + 1 %} {% set n = cfg.count + 1 %}
{% if not p.available %} {% if not p.available %}
{action_raise_error("PURGE_UNTIL_EMPTY: lost the bd_pressure load-check reading, stopped. bdwidth runout response may still be off: SET_FILAMENT_SENSOR SENSOR=<bdwidth name> ENABLE=1")} _ERROR MSG="PURGE_UNTIL_EMPTY: lost the bd_pressure load-check reading, stopped. bdwidth runout response may still be off: SET_FILAMENT_SENSOR SENSOR=<bdwidth name> ENABLE=1" DISPLAY="PURGE_UNTIL_EMPTY: lost bd_pressure, stopped"
{% endif %} {% endif %}
{% set empty = p.longest_ms < cfg.empty_ms %} {% set empty = p.longest_ms < cfg.empty_ms %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE={n} SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE={n}
@@ -123,5 +123,5 @@ gcode:
{% if cfg.done %} {% if cfg.done %}
RESPOND MSG="PURGE_UNTIL_EMPTY: no more filament after {cfg.count} purge{'s' if cfg.count != 1 else ''}. The last bit between the extruder gears and the nozzle is pushed out by the next load." RESPOND MSG="PURGE_UNTIL_EMPTY: no more filament after {cfg.count} purge{'s' if cfg.count != 1 else ''}. The last bit between the extruder gears and the nozzle is pushed out by the next load."
{% else %} {% else %}
RESPOND TYPE=error MSG="PURGE_UNTIL_EMPTY: still pressure after {params.MAX} purges. The lane may still be feeding, or the nozzle is blocked - check before running it again." _WARN MSG="PURGE_UNTIL_EMPTY: still pressure after {params.MAX} purges. The lane may still be feeding, or the nozzle is blocked - check before running it again." DISPLAY="PURGE_UNTIL_EMPTY: still pressure, check lane and nozzle"
{% endif %} {% endif %}
+2 -3
View File
@@ -53,11 +53,10 @@ gcode:
{% endif %} {% endif %}
G1 X{th.axis_maximum.x - 10} Y{th.axis_maximum.y - 10} F5000 G1 X{th.axis_maximum.x - 10} Y{th.axis_maximum.y - 10} F5000
{% elif cfg.soft_fail|int == 1 %} {% elif cfg.soft_fail|int == 1 %}
RESPOND TYPE=error MSG="AFC: XY drift detected! X={'%.3f' % d.x} Y={'%.3f' % d.y} mm. Axes are re-homed, print PAUSED. Run AFC_XY_RESUME to continue, or CANCEL_PRINT." _WARN MSG="AFC: XY drift detected! X={'%.3f' % d.x} Y={'%.3f' % d.y} mm. Axes are re-homed, print PAUSED. Run AFC_XY_RESUME to continue, or CANCEL_PRINT." DISPLAY="AFC: XY drift! See console. Paused."
M117 AFC: XY drift! See console. Paused.
PAUSE PAUSE
{% else %} {% else %}
RESPOND TYPE=error MSG="AFC: XY drift X={'%.3f' % d.x} Y={'%.3f' % d.y} mm -> M112" _WARN MSG="AFC: XY drift X={'%.3f' % d.x} Y={'%.3f' % d.y} mm -> M112"
M112 M112
{% endif %} {% endif %}
+3 -3
View File
@@ -291,9 +291,9 @@ gcode:
# ====================================================================================== # ======================================================================================
{% if "xyz" not in printer.toolhead.homed_axes %} {% if "xyz" not in printer.toolhead.homed_axes %}
RESPOND MSG="BLOBIFIER: Not homed! Home xyz before blobbing" _WARN MSG="BLOBIFIER: Not homed! Home xyz before blobbing"
{% elif printer.quad_gantry_level and printer.quad_gantry_level.applied == False %} {% elif printer.quad_gantry_level and printer.quad_gantry_level.applied == False %}
RESPOND MSG="BLOBIFIER: QGL not applied! run quad_gantry_level before blobbing" _WARN MSG="BLOBIFIER: QGL not applied! run quad_gantry_level before blobbing"
{% else %} {% else %}
# Part cooling fan # Part cooling fan
@@ -780,7 +780,7 @@ gcode:
{% set position_x = bl.skew_correction %} {% set position_x = bl.skew_correction %}
{% if "xyz" not in printer.toolhead.homed_axes %} {% if "xyz" not in printer.toolhead.homed_axes %}
{action_raise_error("BLOBIFIER: Not homed. Home xyz first")} _ERROR MSG="BLOBIFIER: Not homed. Home xyz first"
{% endif %} {% endif %}
SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=last_shake VALUE={count.current_blobs} SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=last_shake VALUE={count.current_blobs}
+1
View File
@@ -12,6 +12,7 @@
##################################################################### #####################################################################
[include save_variables.cfg] # the one [save_variables] for all macros [include save_variables.cfg] # the one [save_variables] for all macros
[include messages.cfg] # _ERROR, _WARN: errors and warnings also on the display
# Blobifier purge bucket (Turtleblobifier for AFC, modified) # Blobifier purge bucket (Turtleblobifier for AFC, modified)
[include blobifier/blobifier.cfg] [include blobifier/blobifier.cfg]
+32
View File
@@ -0,0 +1,32 @@
# Errors and warnings that also show on the display (M117: the status line in
# Mainsail and KlipperScreen, display_status.message)
#
# action_raise_error aborts while the macro is rendered, before any of its
# G-code runs, so an M117 in the same macro never shows. _ERROR sends the
# M117 first and raises the error in a separate macro: the display keeps the
# text and the calling macros are aborted as with action_raise_error.
#
# _ERROR MSG="full text for the console" DISPLAY="short text"
# _WARN MSG="full text for the console" DISPLAY="short text"
#
# DISPLAY defaults to MSG. No double quotes in either (they end the
# parameter). Nothing after an _ERROR in the calling macro runs, but
# anything that macro does while rendering (action_respond_info, ...) has
# already happened: put the rest in an else branch.
[gcode_macro _ERROR]
description: Show DISPLAY (default MSG) on the display, then raise MSG as an error (aborts the calling macros)
gcode:
M117 {params.DISPLAY|default(params.MSG)}
_RAISE_ERROR MSG="{params.MSG}"
[gcode_macro _RAISE_ERROR]
description: Helper for _ERROR: raise MSG as an error
gcode:
{action_raise_error(params.MSG)}
[gcode_macro _WARN]
description: Show MSG as an error line in the console and DISPLAY (default MSG) on the display, without stopping
gcode:
RESPOND TYPE=error MSG="{params.MSG}"
M117 {params.DISPLAY|default(params.MSG)}
+5 -8
View File
@@ -361,7 +361,7 @@ gcode:
SAVE_VARIABLE VARIABLE=pa_lane_{lane}_spool_id VALUE={spool_id} SAVE_VARIABLE VARIABLE=pa_lane_{lane}_spool_id VALUE={spool_id}
{action_respond_info("Tracked lane %s -> spool_id %d" % (lane, spool_id))} {action_respond_info("Tracked lane %s -> spool_id %d" % (lane, spool_id))}
{% else %} {% else %}
{action_raise_error("_PA_TRACK_LANE_SPOOL: LANE parameter required")} _ERROR MSG="_PA_TRACK_LANE_SPOOL: LANE parameter required"
{% endif %} {% endif %}
@@ -404,14 +404,12 @@ gcode:
{% set cfg = printer["gcode_macro _PA_CFG"] %} {% set cfg = printer["gcode_macro _PA_CFG"] %}
{% set vars = printer.save_variables.variables %} {% set vars = printer.save_variables.variables %}
{% if lane == "" %}
{action_raise_error("LANE_CALIBRATE_PA: LANE parameter required")}
{% endif %}
{% set lane_varname = "pa_lane_" ~ lane ~ "_spool_id" %} {% set lane_varname = "pa_lane_" ~ lane ~ "_spool_id" %}
{% set spool_id = vars[lane_varname]|default(0)|int if lane_varname in vars else 0 %} {% set spool_id = vars[lane_varname]|default(0)|int if lane_varname in vars else 0 %}
{% if spool_id == 0 %} {% if lane == "" %}
_ERROR MSG="LANE_CALIBRATE_PA: LANE parameter required"
{% elif spool_id == 0 %}
{action_respond_info("LANE_CALIBRATE_PA: no spool_id recorded for %s yet. Run SET_SPOOL_ID LANE=%s SPOOL_ID=<id> then _PA_TRACK_LANE_SPOOL LANE=%s SPOOL_ID=<id>." % (lane, lane, lane))} {action_respond_info("LANE_CALIBRATE_PA: no spool_id recorded for %s yet. Run SET_SPOOL_ID LANE=%s SPOOL_ID=<id> then _PA_TRACK_LANE_SPOOL LANE=%s SPOOL_ID=<id>." % (lane, lane, lane))}
{% else %} {% else %}
{% set pa_varname = "pa_value_spool_" ~ spool_id %} {% set pa_varname = "pa_value_spool_" ~ spool_id %}
@@ -506,9 +504,8 @@ gcode:
# A cached value at the end of the scan is an earlier failed run, not a PA # A cached value at the end of the scan is an earlier failed run, not a PA
{% set use_cache = cache_found and cached_pa < cfg.sweep_end_pa|float - 0.0001 %} {% set use_cache = cache_found and cached_pa < cfg.sweep_end_pa|float - 0.0001 %}
{% set keep = cached_pa if use_cache else cfg.fallback_pa|float %} {% set keep = cached_pa if use_cache else cfg.fallback_pa|float %}
RESPOND TYPE=error MSG="PA calibration for {lane} (spool {spool_id}) rejected: {problems|join('; ')}. Nothing saved; using PA={'%.4f' % keep} ({'cached' if use_cache else 'fallback'}). Fix the cause, then LANE_CALIBRATE_PA LANE={lane} FORCE=1" _WARN MSG="PA calibration for {lane} (spool {spool_id}) rejected: {problems|join('; ')}. Nothing saved; using PA={'%.4f' % keep} ({'cached' if use_cache else 'fallback'}). Fix the cause, then LANE_CALIBRATE_PA LANE={lane} FORCE=1" DISPLAY="PA cal rejected: {lane}, see console"
SET_PRESSURE_ADVANCE ADVANCE={keep} SET_PRESSURE_ADVANCE ADVANCE={keep}
M117 PA cal rejected: {lane}
{% else %} {% else %}
{action_respond_info("Calibration complete for %s: PA=%.4f" % (lane, new_pa))} {action_respond_info("Calibration complete for %s: PA=%.4f" % (lane, new_pa))}
M117 PA cal done: {new_pa|round(4)} M117 PA cal done: {new_pa|round(4)}
+17 -5
View File
@@ -29,6 +29,12 @@ gcode:
{% if 'AFC' in printer %} {% if 'AFC' in printer %}
{% set lane = printer.AFC.next_lane or printer.AFC.current_load or '' %} {% set lane = printer.AFC.next_lane or printer.AFC.current_load or '' %}
SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=expected_lane VALUE="'{lane}'" SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=expected_lane VALUE="'{lane}'"
# AFC pauses after an error: show its first line. Pauses from these
# macros set their own display text before calling PAUSE.
{% set afc_msg = printer.AFC.message|default({}) %}
{% if printer.AFC.error_state and afc_msg.type|default('') == 'error' and afc_msg.message|default('') %}
M117 Paused: {afc_msg.message.split('\n')[0]|trim}
{% endif %}
{% endif %} {% endif %}
SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=saved_fan_speed VALUE={printer.fan.speed} SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=saved_fan_speed VALUE={printer.fan.speed}
# Pause first, so the saved state is the print position, then retract and park # Pause first, so the saved state is the print position, then retract and park
@@ -85,13 +91,16 @@ gcode:
{% endif %} {% endif %}
SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=parked VALUE=False SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=parked VALUE=False
SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=expected_lane VALUE="''" SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=expected_lane VALUE="''"
# Clear the pause reason from the display (no comment on an M117 line: it would show)
M117
{% endif %} {% endif %}
RESUME_BASE {get_params} RESUME_BASE {get_params}
_status_printing _status_printing
# Raising an error aborts the whole RESUME like a failed purge check: the # Raising an error (_ERROR, also shown on the display) aborts the whole
# printer stays paused and the next RESUME checks again. # RESUME like a failed purge check: the printer stays paused and the next
# RESUME checks again.
[gcode_macro _RESUME_LANE_CHECK] [gcode_macro _RESUME_LANE_CHECK]
description: Stop the resume when AFC has another lane loaded than the one the print needs description: Stop the resume when AFC has another lane loaded than the one the print needs
gcode: gcode:
@@ -101,10 +110,11 @@ gcode:
{% if loaded != expected %} {% if loaded != expected %}
{% set obj = 'AFC_stepper ' ~ expected %} {% set obj = 'AFC_stepper ' ~ expected %}
{% set tool = printer[obj].map if obj in printer else '?' %} {% set tool = printer[obj].map if obj in printer else '?' %}
{action_raise_error("Lane check: the print needs %s (%s) but %s is loaded. Load %s with %s, then RESUME again (RESUME LANE_CHECK=0 resumes anyway)." % (expected, tool, loaded or 'no lane', expected, tool))} _ERROR MSG="Lane check: the print needs {expected} ({tool}) but {loaded or 'no lane'} is loaded. Load {expected} with {tool}, then RESUME again (RESUME LANE_CHECK=0 resumes anyway)." DISPLAY="Lane check: load {expected} with {tool}, then RESUME"
{% endif %} {% else %}
RESPOND MSG="Lane check: {expected} loaded, OK" RESPOND MSG="Lane check: {expected} loaded, OK"
{% endif %} {% endif %}
{% endif %}
[gcode_macro _RESUME_PURGE] [gcode_macro _RESUME_PURGE]
description: Purge LENGTH mm into the Blobifier (with the purge check if installed) before resuming description: Purge LENGTH mm into the Blobifier (with the purge check if installed) before resuming
@@ -124,7 +134,7 @@ gcode:
[gcode_macro _RESUME_PURGE_VERIFY] [gcode_macro _RESUME_PURGE_VERIFY]
gcode: gcode:
{% if printer["gcode_macro _PURGE_CHECK_CFG"].last_failed %} {% if printer["gcode_macro _PURGE_CHECK_CFG"].last_failed %}
{action_raise_error("Purge check failed: the print stays paused. Check the extruder and lane, then RESUME again (RESUME PURGE=0 resumes without purging).")} _ERROR MSG="Purge check failed: the print stays paused. Check the extruder and lane, then RESUME again (RESUME PURGE=0 resumes without purging)." DISPLAY="Purge check failed: still paused, see console"
{% endif %} {% endif %}
[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
@@ -191,6 +201,8 @@ gcode:
# Same cleanup as PRINT_END: heaters incl. chamber heater, fan, flow, bdwidth # Same cleanup as PRINT_END: heaters incl. chamber heater, fan, flow, bdwidth
_PRINT_CLEANUP _PRINT_CLEANUP
SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=parked VALUE=False SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=parked VALUE=False
# Clear a pause reason from the display
M117
_status_ready _status_ready
[gcode_macro M600] [gcode_macro M600]
+3 -1
View File
@@ -26,6 +26,8 @@ gcode:
RESET_FAILURE RESET_FAILURE
CLEAR_PAUSE CLEAR_PAUSE
# Clear what an earlier print or error left on the display
M117
_motion_detected _motion_detected
@@ -299,7 +301,7 @@ gcode:
RESTORE_GCODE_STATE NAME=_nozzle_unretract RESTORE_GCODE_STATE NAME=_nozzle_unretract
SAVE_VARIABLE VARIABLE=nozzle_retract VALUE=0 SAVE_VARIABLE VARIABLE=nozzle_retract VALUE=0
{% else %} {% else %}
RESPOND TYPE=error MSG="_NOZZLE_UNRETRACT: extruder too cold, {done} mm still retracted" _WARN MSG="_NOZZLE_UNRETRACT: extruder too cold, {done} mm still retracted"
{% endif %} {% endif %}
{% endif %} {% endif %}