2026-10-10 13:45:11 +02:00
|
|
|
#####################################################################
|
|
|
|
|
# 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
|
|
|
|
|
|
2026-10-12 21:18:21 +02:00
|
|
|
# Remove this section if bdpressure_check.py is not installed. With it
|
|
|
|
|
# installed, a bd_pressure that does not answer (stock firmware, hung or
|
|
|
|
|
# unplugged sensor) fails the check, so in enforce mode every purge pauses;
|
|
|
|
|
# PURGE_CHECK_IGNORE_SENSOR lets the current print finish without it.
|
2026-10-10 13:45:11 +02:00
|
|
|
[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
|
2026-10-12 14:08:53 +02:00
|
|
|
variable_last_failed: False # last check failed in enforce mode (read by _RESUME_PURGE_VERIFY)
|
2026-10-12 21:18:21 +02:00
|
|
|
variable_ignore_missing_sensor: False # set by PURGE_CHECK_IGNORE_SENSOR, cleared at print end
|
2026-10-19 15:59:27 +02:00
|
|
|
# Hotter purge: the leftover of the previous filament (the piece below the
|
|
|
|
|
# cutter) has to melt to be pushed out. When the filament that last went
|
|
|
|
|
# through the nozzle (saved variable nozzle_last_temp) needs more than the
|
|
|
|
|
# new one plus flush_margin, the purge runs at that temperature and the
|
|
|
|
|
# nozzle cools back to the new filament's afterwards (seen: ASA leftover,
|
|
|
|
|
# TPU loaded at 210C, purge only worked after heating by hand).
|
|
|
|
|
variable_flush_margin: 10 # C; smaller differences purge at the new temperature
|
|
|
|
|
variable_flush_restore: 0 # runtime: temperature to cool back to after a hot purge
|
2026-10-10 13:45:11 +02:00
|
|
|
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.
|
2026-10-12 18:18:40 +02:00
|
|
|
{% set has_bdw = printer.bdwidth_status.present|default(true) %}
|
|
|
|
|
{% set bdw_on = has_bdw and cfg.motion_check and not printer.bdwidth_status.reading|default(false) %}
|
2026-10-10 13:45:11 +02:00
|
|
|
{% 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.
|
2026-10-12 18:18:40 +02:00
|
|
|
{% set runout_off = has_bdw and (bdw_on or not cfg.motion_check) %}
|
2026-10-19 15:59:27 +02:00
|
|
|
_PURGE_FLUSH_HEAT
|
2026-10-10 13:45:11 +02:00
|
|
|
{% 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 %}
|
2026-10-19 15:59:27 +02:00
|
|
|
_PURGE_FLUSH_DONE
|
2026-10-10 13:45:11 +02:00
|
|
|
|
|
|
|
|
# Separate macros so the status is read when they run, not when
|
|
|
|
|
# BLOBIFIER_CHECKED is rendered
|
2026-10-19 15:59:27 +02:00
|
|
|
[gcode_macro _PURGE_FLUSH_HEAT]
|
|
|
|
|
description: Helper for BLOBIFIER_CHECKED: heat to the previous filament's temperature when it is hotter than the new one
|
|
|
|
|
gcode:
|
|
|
|
|
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
|
|
|
|
|
{% set old = printer.save_variables.variables.nozzle_last_temp|default(0)|int %}
|
|
|
|
|
{% set new = printer.extruder.target|int %}
|
|
|
|
|
{% set top = printer.configfile.settings.extruder.max_temp|int - 15 %}
|
|
|
|
|
{% set hot = [old, top]|min %}
|
|
|
|
|
{% if new > 0 and hot > new + cfg.flush_margin %}
|
|
|
|
|
RESPOND MSG="Purge at {hot}C: the previous filament needs {old}C, back to {new}C after the purge"
|
|
|
|
|
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=flush_restore VALUE={new}
|
|
|
|
|
M104 S{hot}
|
|
|
|
|
TEMPERATURE_WAIT SENSOR=extruder MINIMUM={hot - 2}
|
|
|
|
|
{% else %}
|
|
|
|
|
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=flush_restore VALUE=0
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
[gcode_macro _PURGE_FLUSH_DONE]
|
|
|
|
|
description: Helper for BLOBIFIER_CHECKED: cool back after a hot purge and remember the new filament's temperature
|
|
|
|
|
gcode:
|
|
|
|
|
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
|
|
|
|
|
{% set new = cfg.flush_restore|int if cfg.flush_restore|int > 0 else printer.extruder.target|int %}
|
|
|
|
|
{% if cfg.flush_restore|int > 0 %}
|
|
|
|
|
M104 S{new}
|
|
|
|
|
TEMPERATURE_WAIT SENSOR=extruder MAXIMUM={new + 3}
|
|
|
|
|
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=flush_restore VALUE=0
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if new > 0 %}
|
|
|
|
|
SAVE_VARIABLE VARIABLE=nozzle_last_temp VALUE={new}
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
2026-10-10 13:45:11 +02:00
|
|
|
[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 %}
|
2026-10-12 21:18:21 +02:00
|
|
|
{% elif 'bdpressure_check' in printer %}
|
|
|
|
|
{% if cfg.ignore_missing_sensor %}
|
|
|
|
|
{% set press = "pressure skipped: bd_pressure not answering (ignored, PURGE_CHECK_IGNORE_SENSOR)" %}
|
|
|
|
|
{% else %}
|
|
|
|
|
{% set press = "bd_pressure not answering" %}
|
|
|
|
|
{% set _ = failed.append("bd_pressure not answering (power-cycle the sensor, or PURGE_CHECK_IGNORE_SENSOR to finish this print without the check)") %}
|
|
|
|
|
{% endif %}
|
2026-10-10 13:45:11 +02:00
|
|
|
{% else %}
|
2026-10-12 21:18:21 +02:00
|
|
|
{% set press = "pressure skipped (no bdpressure_check)" %}
|
2026-10-10 13:45:11 +02:00
|
|
|
{% 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" %}
|
2026-10-16 17:41:41 +02:00
|
|
|
_WARN MSG="PURGE_CHECK: {lane} {failed|join(', ')} - check extruder and lane" DISPLAY="Purge check failed: {lane}, paused"
|
2026-10-12 14:08:53 +02:00
|
|
|
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=last_failed VALUE=True
|
|
|
|
|
PAUSE ; no effect while already paused (resume purge): see _RESUME_PURGE_VERIFY
|
2026-10-10 13:45:11 +02:00
|
|
|
{% endif %}
|
|
|
|
|
{% endif %}
|
2026-10-12 21:18:21 +02:00
|
|
|
|
|
|
|
|
[gcode_macro PURGE_CHECK_IGNORE_SENSOR]
|
|
|
|
|
description: Let purges pass without bd_pressure until the print ends (ENABLE=0 to undo)
|
|
|
|
|
gcode:
|
|
|
|
|
{% if params.ENABLE|default(1)|int %}
|
|
|
|
|
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=ignore_missing_sensor VALUE=True
|
|
|
|
|
RESPOND MSG="PURGE_CHECK: an unavailable bd_pressure is ignored until the print ends (purges are not checked for pressure then)"
|
|
|
|
|
{% else %}
|
|
|
|
|
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=ignore_missing_sensor VALUE=False
|
|
|
|
|
RESPOND MSG="PURGE_CHECK: an unavailable bd_pressure fails the check again"
|
|
|
|
|
{% endif %}
|