Group macros by topic, add Blobifier, fix first tool change

- Split the macro files into folders by topic (print, heating, afc,
  pressure_advance, spoolman, leds, sensors); klipper-macros.cfg is the
  one file printer.cfg includes. Macros moved unchanged; the effective
  config is identical apart from the paths of the PA shell scripts.
- Add the modified Blobifier (Turtleblobifier for AFC) and define
  [save_variables] once in save_variables.cfg instead of in both
  blobifier.cfg and the PA macros (same file, bd_pa_variables.cfg).
- PRINT_WARMUP: heat to the loaded lane's temperature before the first
  tool change. AFC skips heating when the loaded lane's tool number is
  not in the print file's temperature list, and the unload then failed
  below min_extrude_temp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-10 14:24:37 +02:00
1 parent 6dbb4ca3fb
commit eaf9c82617
27 files changed
+1366 -340

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
# AFC lane tools: LANE_FEED_UNTIL_HUB_OPEN, LANE_FEED_ABORT
#####################################################################
## unloading help
#####################################################################
[gcode_macro LANE_FEED_UNTIL_HUB_OPEN]
variable_lane: 1
variable_step: -50
variable_max_dist: 2000
variable_moved: 0
variable_active: 0
gcode:
{% set lane = params.LANE|default(1)|int %}
{% set step = params.STEP|default(-50)|float %}
{% set max_dist = params.MAX_DIST|default(2000)|float %}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=lane VALUE={lane}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=step VALUE={step}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=max_dist VALUE={max_dist}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=moved VALUE=0
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=1
RESPOND PREFIX="info" MSG="START lane={lane} step={step} max={max_dist}"
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0.05
[delayed_gcode _lane_feed_loop]
gcode:
{% set m = printer["gcode_macro LANE_FEED_UNTIL_HUB_OPEN"] %}
{% if m.active == 1 %}
LANE_MOVE LANE=lane{m.lane} DISTANCE={m.step}
{% set new_moved = m.moved + m.step %}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=moved VALUE={new_moved}
RESPOND PREFIX="info" MSG="MOVE lane={m.lane} step={m.step} moved={new_moved}"
QUERY_ENDSTOPS
UPDATE_DELAYED_GCODE ID=_lane_eval DURATION=0.03
{% else %}
RESPOND PREFIX="info" MSG="feed_loop inactive"
{% endif %}
[delayed_gcode _lane_eval]
gcode:
{% set m = printer["gcode_macro LANE_FEED_UNTIL_HUB_OPEN"] %}
{% if m.active == 1 %}
{% set hub = printer.query_endstops.last_query.get("hub", "unknown") %}
# RESPOND PREFIX="info" MSG="QUERY hub={hub} raw={printer.query_endstops.last_query.get('hub', 'MISSING')}"
# RESPOND PREFIX="info" MSG="last_query: {printer.query_endstops.last_query}"
{% if hub == "open" or hub == 0 %}
RESPOND PREFIX="info" MSG="HUB TRIGGERED → STOP"
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
LANE_MOVE LANE=lane{m.lane} DISTANCE=-100
{% elif m.moved|abs >= m.max_dist %}
RESPOND PREFIX="warning" MSG="MAX DIST STOP reached={m.moved}"
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
{% else %}
RESPOND PREFIX="info" MSG="CONTINUE"
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0.03
{% endif %}
{% else %}
RESPOND PREFIX="info" MSG="eval inactive"
{% endif %}
[gcode_macro LANE_FEED_ABORT]
gcode:
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0
UPDATE_DELAYED_GCODE ID=_lane_eval DURATION=0
RESPOND PREFIX="warning" MSG="ABORTED"
# [gcode_macro TEST_AMS_STRESS]
# gcode:
# {% for i in range(500) %}
# T0 ; Load tool 0
# G4 P5000 ; Wait 5s
# T1 ; Unload
# G4 P3000 ; Wait 3s
# {% endfor %}
+155
View File
@@ -0,0 +1,155 @@
#####################################################################
# Purge check - did the filament really reach the nozzle during the
# Blobifier purge after a tool change?
#
# Two independent checks around the purge AFC runs after every load:
# 1. bdwidth motion: extruded mm vs mm the sensor saw moving past it.
# Catches a grinding extruder, a tangle or a snapped filament.
# 2. bd_pressure (needs the load-check firmware extension): how long
# the nozzle pressure stayed above the threshold. Catches filament
# that never reached the melt zone. Skipped automatically on stock
# firmware or when bdpressure_check.py is not installed.
#
# Needs: bdwidth_status.py in ~/klipper/klippy/extras/
# bdpressure_check.py in ~/klipper/klippy/extras/ (optional)
# (klipper/install.sh links both; guide: klipper/load_check/afc_blobifier/README.md)
# Hook: in [AFC] (AFC/AFC.cfg) change poop_cmd: BLOBIFIER
# to poop_cmd: BLOBIFIER_CHECKED
#
# Rollout:
# Phase 1 mode "report": only prints the results after every purge.
# Collect a few days of values, then set min_ratio and
# pressure_min_ms well below the lowest good ones.
# Phase 2 mode "enforce": pauses when a check fails.
#
# Costs about 2 x (sample_time + 0.5 s) per swap, 5 s at sample_time 2,
# so the sensors are quiet and up to date before and after the purge.
# When bdwidth is not reading, add bdwidth_warmup_ms: it is switched on for
# the check (motion only, SET_BDWIDTH ENABLE_MOTION) with its runout pause
# off meanwhile (SET_FILAMENT_SENSOR), and outside a print off again after.
#####################################################################
[bdwidth_status]
sensor: fila_width_0
# Remove this section if bdpressure_check.py is not installed
[bdpressure_check]
sensor: bd_pa
[gcode_macro _PURGE_CHECK_CFG]
variable_mode: "enforce" # report | enforce | off
variable_motion_check: False # False: skip the bdwidth motion check, pressure only
variable_min_ratio: 0.4 # placeholder: sensed mm / extruded mm below this = not feeding
variable_min_extrude: 20 # skip the checks for purges shorter than this (mm)
variable_pressure_min_ms: 5000 # placeholder: longest continuous pressure needed (ms)
# bdwidth is not reading after SET_BDWIDTH ... COMMAND=DISABLE and after
# every Klipper restart until an ENABLE (outside a print, often early in a
# print start). The check then switches it on and waits this long for the
# first readings (its first read often fails and it retries after 10 s).
variable_bdwidth_warmup_ms: 15000
variable_start_counts: 0
variable_start_e: 0.0
gcode:
[gcode_macro BLOBIFIER_CHECKED]
description: Blobifier purge with bdwidth motion and bd_pressure checks (use as poop_cmd in [AFC])
gcode:
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
{% set wait = (printer.bdwidth_status.sample_time * 1000 + 500)|int %}
{% set pressure = 'bdpressure_check' in printer %}
{% set bdw = printer.bdwidth_status.name %}
# If bdwidth is not reading (outside a print, early in a print start,
# after a Klipper restart) it is switched on for the check, with its
# runout response off meanwhile, so an empty lane does not make bdwidth
# pause the printer (the check reports it instead). Afterwards the
# runout response is restored; bdwidth itself is only switched off
# again outside a print, so a print never loses it.
{% set bdw_on = cfg.motion_check and not printer.bdwidth_status.reading|default(false) %}
{% set bdw_off = bdw_on and printer.print_stats.state not in ["printing", "paused"] %}
{% set runout_was = 1 if printer.bdwidth_status.enabled else 0 %}
# Without the motion check, bdwidth's own runout response is off during
# the purge too, so a bdwidth that does not see the movement cannot
# pause the printer here; the pressure check judges the purge.
{% set runout_off = bdw_on or not cfg.motion_check %}
{% if cfg.mode == "off" %}
BLOBIFIER {rawparams}
{% else %}
M400
{% if runout_off %}
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE=0
{% endif %}
{% if bdw_on %}
SET_BDWIDTH NAME={bdw} COMMAND=ENABLE_MOTION
G4 P{cfg.bdwidth_warmup_ms}
{% endif %}
G4 P{wait}
_PURGE_CHECK_START
{% if pressure %}
BDP_CHECK_START
{% endif %}
BLOBIFIER {rawparams}
M400
{% if pressure %}
BDP_CHECK_STOP
{% endif %}
G4 P{wait}
_PURGE_CHECK_END
{% if bdw_off %}
SET_BDWIDTH NAME={bdw} COMMAND=DISABLE
{% endif %}
{% if runout_off %}
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE={runout_was}
{% endif %}
{% endif %}
# Separate macros so the status is read when they run, not when
# BLOBIFIER_CHECKED is rendered
[gcode_macro _PURGE_CHECK_START]
gcode:
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=start_counts VALUE={printer.bdwidth_status.total_move}
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=start_e VALUE={printer.toolhead.position.e}
[gcode_macro _PURGE_CHECK_END]
gcode:
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
{% set s = printer.bdwidth_status %}
{% set lane = printer.AFC.current_load|default("?", true) %}
{% set extruded = printer.toolhead.position.e - cfg.start_e %}
{% set failed = [] %}
{% if extruded < cfg.min_extrude %}
RESPOND MSG="PURGE_CHECK {lane}: only {'%.1f' % extruded} mm extruded, skipped"
{% else %}
{% set motion_on = cfg.motion_check and s.active != "disable" %}
{% if not cfg.motion_check %}
{% set motion = "motion check off" %}
{% elif not motion_on %}
{% set motion = "motion skipped (bdwidth disabled)" %}
{% else %}
{% set sensed = (s.total_move - cfg.start_counts)|abs / s.linear_motion %}
{% set ratio = sensed / extruded %}
{% set motion = "motion %.1f/%.1f mm ratio %.2f, width %.2f" % (sensed, extruded, ratio, s.diameter) %}
{% if ratio < cfg.min_ratio %}
{% if s.total_move == 0 and s.diameter == 0 %}
{% set _ = failed.append("no data from bdwidth since Klipper start (sensor connected?)") %}
{% else %}
{% set _ = failed.append("filament not moving past bdwidth") %}
{% endif %}
{% endif %}
{% endif %}
{% if 'bdpressure_check' in printer and printer.bdpressure_check.available %}
{% set p = printer.bdpressure_check %}
{% set press = "pressure longest %d ms, total %d ms, peak %d" % (p.longest_ms, p.above_ms, p.peak) %}
{% if p.longest_ms < cfg.pressure_min_ms %}
{% set _ = failed.append("no sustained nozzle pressure") %}
{% endif %}
{% else %}
{% set press = "pressure skipped (no check firmware)" %}
{% endif %}
{% set checked = motion_on or 'bdpressure_check' in printer and printer.bdpressure_check.available %}
{% set verdict = 'FAIL: ' ~ failed|join(', ') if failed else ('OK' if checked else 'NOTHING CHECKED') %}
RESPOND MSG="PURGE_CHECK {lane}: {motion}; {press} -> {verdict}"
{% if failed and cfg.mode == "enforce" %}
RESPOND TYPE=error MSG="PURGE_CHECK: {lane} {failed|join(', ')} - check extruder and lane"
PAUSE
{% endif %}
{% endif %}
+127
View File
@@ -0,0 +1,127 @@
#####################################################################
# PURGE_UNTIL_EMPTY - purge a broken filament out through the nozzle
#
# When the filament snaps inside the PTFE tube, the piece below the break
# is still in the extruder. This macro repeats the Blobifier purge and
# measures each one with bd_pressure (load-check firmware). It stops after
# the first purge without sustained nozzle pressure: the extruder no longer
# has filament to push.
#
# What stays behind: the extruder can only push filament its gears still
# grip. Once the tail has passed the gears, the last bit between the gears
# and the nozzle stays in the hotend. The next normal load pushes it out.
#
# The AFC lane is unsynced first (UNSET_LANE_LOADED). Otherwise AFC keeps
# feeding the lane together with the extruder, the spool side of the break
# can catch up and the purge never ends. Afterwards AFC sees no lane loaded:
# eject or reload the lane as usual.
#
# bdwidth sits at the start of the tube, so after the break it sees no
# filament while the extruder keeps pushing. Its runout response (motion or
# width) would pause the printer mid-purge, so it is switched off during the
# purges (SET_FILAMENT_SENSOR) and restored afterwards. bdwidth is not used
# to decide when to stop: only the nozzle pressure can see the last piece.
#
# Needs: bdpressure_check.py + [bdpressure_check] (see purge_check.cfg)
# Blobifier, homed printer, hot nozzle (the macro heats if needed)
#
# Usage (printer idle or paused, not while printing):
# PURGE_UNTIL_EMPTY defaults below
# PURGE_UNTIL_EMPTY TEMP=240 MAX=10 filament temperature, purge limit
# PURGE_UNTIL_EMPTY LENGTH=100 mm per purge (default: Blobifier's)
# PURGE_UNTIL_EMPTY UNSYNC=0 keep the AFC lane synced
#
# Each purge prints one line; the end result says whether the nozzle ran
# empty or MAX purges were reached (lane still feeding, or nozzle blocked).
#####################################################################
[gcode_macro PURGE_UNTIL_EMPTY]
description: Repeat the Blobifier purge until bd_pressure measures no more filament
variable_max_purges: 15 # safety limit: ~1.2 m of bowden is ~8 purges of 150 mm
variable_empty_ms: 5000 # longest pressure below this = no filament (good purge ~17 s, empty ~30 ms)
variable_done: 0
variable_count: 0
gcode:
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
{% set max_purges = params.MAX|default(cfg.max_purges)|int %}
{% set length = params.LENGTH|default(0)|float %}
{% set unsync = params.UNSYNC|default(1)|int %}
{% set min_temp = printer.configfile.settings.extruder.min_extrude_temp|float %}
{% set temp = params.TEMP|default([printer.extruder.target, 0]|max)|float %}
{% if 'bdpressure_check' not in printer %}
{action_raise_error("PURGE_UNTIL_EMPTY: needs bdpressure_check.py and [bdpressure_check]")}
{% elif printer.print_stats.state == "printing" %}
{action_raise_error("PURGE_UNTIL_EMPTY: printer is printing, pause the print first")}
{% elif "xyz" not in printer.toolhead.homed_axes %}
{action_raise_error("PURGE_UNTIL_EMPTY: home the printer first")}
{% elif temp < min_temp %}
{action_raise_error("PURGE_UNTIL_EMPTY: give the filament temperature, e.g. PURGE_UNTIL_EMPTY TEMP=240")}
{% else %}
BDP_CHECK_QUERY
_PURGE_UNTIL_EMPTY_RUN MAX={max_purges} LENGTH={length} UNSYNC={unsync} TEMP={temp}
{% endif %}
# Rendered after BDP_CHECK_QUERY, so 'available' is current.
[gcode_macro _PURGE_UNTIL_EMPTY_RUN]
gcode:
{% if not printer.bdpressure_check.available %}
{action_raise_error("PURGE_UNTIL_EMPTY: bd_pressure load-check firmware not found")}
{% endif %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=0
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE=0
{% if params.UNSYNC|int and printer.AFC.current_load %}
RESPOND MSG="PURGE_UNTIL_EMPTY: unsyncing {printer.AFC.current_load} from the extruder (UNSET_LANE_LOADED)"
UNSET_LANE_LOADED
{% endif %}
{% set bdw = printer.bdwidth_status.name if 'bdwidth_status' in printer else '' %}
{% set runout_was = 1 if bdw and printer.bdwidth_status.enabled else 0 %}
{% if runout_was %}
RESPOND MSG="PURGE_UNTIL_EMPTY: bdwidth runout response off during the purges"
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE=0
{% endif %}
M109 S{params.TEMP}
{% for i in range(params.MAX|int) %}
_PURGE_UNTIL_EMPTY_STEP LENGTH={params.LENGTH}
{% endfor %}
_PURGE_UNTIL_EMPTY_RESULT MAX={params.MAX} BDW={bdw if runout_was else '-'}
# Rendered when it runs, so it sees the result of the previous purge.
[gcode_macro _PURGE_UNTIL_EMPTY_STEP]
gcode:
{% if not printer["gcode_macro PURGE_UNTIL_EMPTY"].done %}
{% set length = params.LENGTH|default(0)|float %}
M400
G4 P500
BDP_CHECK_START
BLOBIFIER {'PURGE_LENGTH=%.1f' % length if length > 0 else ''}
M400
BDP_CHECK_STOP
_PURGE_UNTIL_EMPTY_EVAL
{% endif %}
[gcode_macro _PURGE_UNTIL_EMPTY_EVAL]
gcode:
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
{% set p = printer.bdpressure_check %}
{% set n = cfg.count + 1 %}
{% 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")}
{% endif %}
{% set empty = p.longest_ms < cfg.empty_ms %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE={n}
RESPOND MSG="PURGE_UNTIL_EMPTY purge {n}: pressure longest {p.longest_ms} ms, total {p.above_ms} ms, peak {p.peak} -> {'empty' if empty else 'filament'}"
{% if empty %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=1
{% endif %}
[gcode_macro _PURGE_UNTIL_EMPTY_RESULT]
gcode:
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
{% if params.BDW|default('-') != '-' %}
SET_FILAMENT_SENSOR SENSOR={params.BDW} ENABLE=1
{% endif %}
{% 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."
{% 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."
{% endif %}
+70
View File
@@ -0,0 +1,70 @@
# AFC_XY_SAFETY_CHECK: lost-step check around tool changes (needs extras/xy_drift.py)
#####################################################################
## XY lost-step check (needs xy_drift.py + [xy_drift])
#####################################################################
# Homing resets the toolhead position, so the position after G28 cannot
# show lost steps. [xy_drift] compares the stepper step counts at the X and
# Y endstop triggers instead: every lost step shows up as drift.
# _AFC_STORE_HOME_REFERENCE (PRINT_START) stores the reference, every
# AFC_XY_SAFETY_CHECK compares against it and then stores a new reference,
# so each check reports the drift since the previous one.
[xy_drift]
[gcode_macro _AFC_STORE_HOME_REFERENCE]
gcode:
G28 X Y
M400
XY_DRIFT_STORE
[gcode_macro AFC_XY_SAFETY_CHECK]
description: Re-home XY and report steps lost since the last check
variable_allowed_mm: 0.1 # one motor skipping one cycle (0.4 mm) shows as 0.2 mm in X and Y; endstop noise ~0.02
variable_soft_fail: 1 # 1 = PAUSE on drift, 0 = M112
gcode:
{% set th = printer.toolhead %}
M117 AFC XY check...
SAVE_GCODE_STATE NAME=_AFC_XY_CHECK
G90
G1 Z{[th.position.z + 10, th.axis_maximum.z]|min} F3000
G28 X Y
M400
_AFC_CHECK_CAPTURE
RESTORE_GCODE_STATE NAME=_AFC_XY_CHECK
[gcode_macro _AFC_CHECK_CAPTURE]
gcode:
{% set d = printer.xy_drift %}
{% set cfg = printer["gcode_macro AFC_XY_SAFETY_CHECK"] %}
{% set th = printer.toolhead %}
{% set lim = cfg.allowed_mm|float %}
{% set drift = d.ref_set and (d.x|abs > lim or d.y|abs > lim) %}
{% if not d.ref_set %}
RESPOND PREFIX="AFC" MSG="XY check: no reference yet (Klipper restarted?), storing one now"
{% else %}
RESPOND PREFIX="AFC" MSG="XY drift since last check: X={'%.3f' % d.x} Y={'%.3f' % d.y} mm (limit {lim})"
{% endif %}
# New reference from this homing, so the next check reports only new drift
XY_DRIFT_STORE
{% if not drift %}
{% if d.ref_set %}
RESPOND PREFIX="AFC" MSG="Position OK"
{% endif %}
G1 X{th.axis_maximum.x - 10} Y{th.axis_maximum.y - 10} F5000
{% 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."
M117 AFC: XY drift! See console. Paused.
PAUSE
{% else %}
RESPOND TYPE=error MSG="AFC: XY drift X={'%.3f' % d.x} Y={'%.3f' % d.y} mm -> M112"
M112
{% endif %}
[gcode_macro AFC_XY_RESUME]
description: Acknowledge XY drift warning and resume print
gcode:
RESPOND PREFIX="AFC" MSG="User acknowledged drift, resuming."
# Axes were re-homed by the check, so AFC's resume returns to the
# correct print position.
RESUME