Print start: contact homing, nozzle guard, Z check, purge before chamber heat

- HOME always homes Z with Beacon contact and recalibrates, also when the
  printer is already homed (home_method_when_homed = proximity used the
  model of an earlier print without touching the bed). PRINT_START homes
  Z again after QGL, before the brush takes the nozzle below Z0.
- _Z_CHECK: after the soak, compare Klipper's Z with a BEACON_QUERY
  reading at Z3 over the bed centre and stop the print above 1 mm.
  Warns and continues when BEACON_QUERY leaves no new
  printer.beacon.last_sample.
- Nozzle guard (extras/bdp_guard.py, print/nozzle_guard.cfg): reads the
  bd_pressure force over I2C every 20 ms while armed and, in stop mode,
  shuts Klipper down on a touch. Armed for HOME X/Y, QGL, Z homing, brush,
  mesh and the Z check; starts in log mode to collect peaks per phase.
- Chamber prints: load and purge the first tool before the chamber heats
  (_LOAD_FOR_CHAMBER replaces _UNLOAD_FOR_CHAMBER). A cut unload leaves a
  piece of PLA in the extruder that softened in the hot chamber and jammed
  the ASA load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-18 22:39:26 +02:00
1 parent 2100857846
commit 61fa8acb05
7 files changed
+374 -26

No files matched your search

+71 -1
View File
@@ -1,8 +1,78 @@
# HOME: home all axes and move to the bed centre
#
# Z always with Beacon contact (the nozzle touches the bed), also when the
# printer is already homed. Without METHOD=CONTACT, [beacon]
# home_method_when_homed = proximity homes a homed printer on the
# eddy-current model of an earlier print (other bed temperature, maybe
# another plate) without touching the bed and without recalibrating.
# Contact refuses a nozzle above 180C (contact_max_hotend_temperature).
[gcode_macro HOME]
gcode:
G90
G4 P2000
G28 X0 Y0 Z0
_GUARD_ON PHASE=home_xy
G28 X Y
# Contact homing presses the nozzle on the bed: guard off
_GUARD_OFF
G28 Z METHOD=CONTACT CALIBRATE=1
_GUARD_ON PHASE=home_lift
G1 X175 Y175 Z10 F1200
_GUARD_OFF
# _Z_CHECK: compare where Klipper thinks the nozzle is with what the Beacon
# measures (proximity), at X/Y (default the bed centre) and Z (default 3 mm,
# inside the model range 0.2-5 mm). Catches a Z that changed since the last
# homing (lost Z steps, gantry moved, much thermal drift) before the nozzle
# goes low; a difference above MAX stops the print. A wrong Beacon model
# fools both readings the same way, so this does not replace contact homing.
#
# BEACON_QUERY stores its reading in printer.beacon.last_sample. When it
# gives no new reading there, the check only warns, so a Beacon version
# without that field does not stop every print.
[gcode_macro _Z_CHECK]
description: Compare Klipper's Z with the Beacon reading at X Y Z (default 175 175 3), stop the print when they differ by more than MAX (default 0.5 mm)
gcode:
{% set x = params.X|default(175)|float %}
{% set y = params.Y|default(175)|float %}
{% set z = params.Z|default(3)|float %}
{% set max = params.MAX|default(0.5)|float %}
{% if 'xyz' not in printer.toolhead.homed_axes %}
_ERROR MSG="_Z_CHECK: printer not homed" DISPLAY="Z check: not homed"
{% elif 'beacon' not in printer %}
_WARN MSG="_Z_CHECK: no [beacon], check skipped"
{% else %}
{% set last = printer.beacon.last_sample %}
{% set prev = last.time if last and last.time is defined else -1 %}
SAVE_GCODE_STATE NAME=_z_check
G90
_GUARD_ON PHASE=z_check
{% if printer.toolhead.position.z < z %}
G1 Z{z} F600
{% endif %}
G1 X{x} Y{y} F12000
G1 Z{z} F600
_GUARD_OFF
G4 P300
BEACON_QUERY
_Z_CHECK_EVAL MAX={max} PREV={prev}
RESTORE_GCODE_STATE NAME=_z_check
{% endif %}
# Separate macro: rendered after BEACON_QUERY has run. toolhead.position is
# the real nozzle height (gcode offset and mesh included).
[gcode_macro _Z_CHECK_EVAL]
description: Helper for _Z_CHECK: compare the BEACON_QUERY reading with the toolhead Z
gcode:
{% set s = printer.beacon.last_sample %}
{% set z = printer.toolhead.position.z %}
{% set max = params.MAX|default(0.5)|float %}
{% if not s or s.time is not defined or s.dist is not defined or s.time == params.PREV|default(-1)|float %}
_WARN MSG="_Z_CHECK: BEACON_QUERY left no new reading in printer.beacon.last_sample, Z check skipped" DISPLAY="Z check skipped"
{% elif s.dist is none %}
_ERROR MSG="_Z_CHECK: Klipper Z={'%.2f' % z} but the Beacon reads nothing (more than 5 mm away or no model), print stopped" DISPLAY="Z check failed, print stopped"
{% elif (s.dist - z)|abs > max %}
_ERROR MSG="_Z_CHECK: Klipper Z={'%.3f' % z}, Beacon measures {'%.3f' % s.dist} mm (difference {'%.3f' % (s.dist - z)}, limit {max}), print stopped" DISPLAY="Z check failed, print stopped"
{% else %}
RESPOND MSG="Z check: Klipper Z={'%.3f' % z}, Beacon {'%.3f' % s.dist} mm, difference {'%.3f' % (s.dist - z)} (limit {max})"
{% endif %}
+33
View File
@@ -0,0 +1,33 @@
# Nozzle collision guard: bd_pressure watches the nozzle force during moves
# that must not touch anything (needs extras/bdp_guard.py and the bd_pressure
# load-check firmware). See extras/bdp_guard.py.
#
# mode: log only measures and reports each phase (console and klippy.log,
# "bdp_guard:"). Run a few print starts in log mode, set threshold well
# above the peaks the phases show, then switch to mode: stop.
[bdp_guard]
sensor: bd_pa
mode: log
threshold: 30
samples: 3
interval: 0.02
# M400 first: BDP_GUARD_ARM/DISARM act at once, not in the move queue, so
# without it the guard would watch moves that are still queued (contact
# homing, extrusion) or stop watching before the armed moves ran.
[gcode_macro _GUARD_ON]
description: Arm the nozzle guard for PHASE=<name> [MODE=log|stop] after the queued moves (nothing without [bdp_guard])
gcode:
{% if 'bdp_guard' in printer %}
M400
BDP_GUARD_ARM PHASE={params.PHASE|default('armed')} {('MODE=' ~ params.MODE) if 'MODE' in params else ''}
{% endif %}
[gcode_macro _GUARD_OFF]
description: Disarm the nozzle guard after the queued moves (nothing without [bdp_guard])
gcode:
{% if 'bdp_guard' in printer %}
M400
BDP_GUARD_DISARM
{% endif %}
+69 -24
View File
@@ -9,10 +9,13 @@
# PRINT_START runs on a soaked bed:
# PRINT_WARMUP bed heats while the nozzle cools and the printer homes,
# wait for the bed, HEAT_SOAK
# PRINT_START QGL, contact Z calibration, mesh, load the initial tool,
# PRINT_START QGL, Z, contact Z calibration, mesh, load the initial tool,
# PA for all lanes, XY reference
# The filament is loaded after the Z calibration, so the contact always
# touches a clean nozzle that has not been oozing at 150C for minutes.
# Chamber prints are the exception: PRINT_WARMUP loads and purges the first
# tool before the chamber heats (_LOAD_FOR_CHAMBER), retracted for the soak;
# BLOBIFIER_CLEAN cleans the nozzle before the contact.
[gcode_macro PRINT_WARMUP]
description: Print start before HEAT_SOAK: home while the bed heats, wait for the bed, soak
@@ -50,9 +53,11 @@ gcode:
RESPOND MSG="homing"
HOME
{% if target_chamber > 30 %}
_UNLOAD_FOR_CHAMBER TOOL={initial_tool}
_LOAD_FOR_CHAMBER TOOL={initial_tool}
{% endif %}
_GUARD_ON PHASE=warmup_park
G1 X175 Y175 Z5 F1200
_GUARD_OFF
_STATUS STATE=heating_bed
{% if target_bed > 0 %}
@@ -71,35 +76,56 @@ gcode:
HEAT_SOAK HEATER='heater_bed' TARGET={target_bed} SOAKER='temperature_sensor bed_core' SOAK_TEMP={target_bed - 5} TIMEOUT=30 CANCEL='_NOTHING'
{% endif %}
# With the chamber heated, a filament that stays in the toolhead path softens
# in the PTFE tube (PLA loaded, ASA print). PRINT_START would swap a lane that
# is not the first tool's anyway, so unload it now, before the chamber heats.
# AFC moves the filament back into the Box Turtle, outside the chamber.
[gcode_macro _UNLOAD_FOR_CHAMBER]
description: Before a chamber soak: unload the toolhead when its lane is not mapped to TOOL (the print's first tool)
# With the chamber heated, filament in the toolhead path softens: a PLA lane
# still loaded softens in the PTFE tube, and the piece of PLA that a cut
# unload leaves below the cutter softens in the extruder and jams the next
# load (seen: PLA unloaded, ASA jammed after the soak). The piece stays until
# the next load, so an empty toolhead can have it too. For a chamber print,
# load the print's first tool and purge it now, before the chamber heats:
# the purge at the new filament's temperature pushes the leftover out.
# AFC unloads a loaded lane back into the Box Turtle, outside the chamber.
[gcode_macro _LOAD_FOR_CHAMBER]
description: Before a chamber soak: load and purge TOOL (the print's first tool) unless its lane is already loaded
gcode:
{% set tool = 'T' ~ params.TOOL|default(0)|int %}
{% set cur = printer.AFC.current_load if 'AFC' in printer else none %}
{% if cur and ('AFC_stepper ' ~ cur) in printer %}
{% set lane = printer['AFC_stepper ' ~ cur] %}
{% if lane.map != tool %}
RESPOND MSG="Chamber heating: unloading {cur} ({lane.material}, {lane.map}), the print starts with {tool}"
_HEAT_FOR_LOADED_LANE TOOL={params.TOOL|default(0)|int}
TOOL_UNLOAD LANE={cur}
_UNLOAD_FOR_CHAMBER_VERIFY LANE={cur}
{% if 'AFC' in printer %}
{% set cur = printer.AFC.current_load %}
{% set ns = namespace(lane=none) %}
{% for l in printer.AFC.lanes if ('AFC_stepper ' ~ l) in printer and printer['AFC_stepper ' ~ l].map == tool %}
{% set ns.lane = l %}
{% endfor %}
{% if ns.lane is none %}
_ERROR MSG="Chamber heating: no AFC lane is mapped to {tool}, print stopped before the chamber heats" DISPLAY="No lane for {tool}, print stopped"
{% elif cur != ns.lane %}
{% set new = printer['AFC_stepper ' ~ ns.lane] %}
# Hot enough to unload the old filament and to purge the new one
{% set temps = [new.extruder_temp|default(0, true)|int, printer.configfile.settings.extruder.min_extrude_temp|int + 20] %}
{% if cur and ('AFC_stepper ' ~ cur) in printer %}
{% set temps = temps + [printer['AFC_stepper ' ~ cur].extruder_temp|default(0, true)|int] %}
RESPOND MSG="Chamber heating: unloading {cur} ({printer['AFC_stepper ' ~ cur].material}), loading and purging {ns.lane} ({new.material}, {tool}) before the chamber heats"
{% else %}
RESPOND MSG="Chamber heating: loading and purging {ns.lane} ({new.material}, {tool}) before the chamber heats"
{% endif %}
M109 S{temps|max}
{tool}
_LOAD_FOR_CHAMBER_VERIFY LANE={ns.lane}
{% endif %}
{% endif %}
# Separate macro: rendered after TOOL_UNLOAD has run. A failed unload stops
# the print before the chamber heats.
[gcode_macro _UNLOAD_FOR_CHAMBER_VERIFY]
description: Helper for _UNLOAD_FOR_CHAMBER: stop the print when the lane is still loaded
# Separate macro: rendered after the tool change has run. AFC pauses on a
# failed load instead of raising an error, so check the result here: a
# failed load stops the print before the chamber heats.
[gcode_macro _LOAD_FOR_CHAMBER_VERIFY]
description: Helper for _LOAD_FOR_CHAMBER: stop the print when LANE is not loaded, else retract and cool the nozzle for the soak
gcode:
{% if printer.AFC.current_load %}
_ERROR MSG="Chamber heating: {params.LANE} could not be unloaded ({printer.AFC.current_load} still in the toolhead), print stopped before the chamber heats" DISPLAY="Unload failed, print stopped"
{% if printer.AFC.current_load != params.LANE %}
_ERROR MSG="Chamber heating: {params.LANE} could not be loaded ({printer.AFC.current_load or 'nothing'} in the toolhead), print stopped before the chamber heats" DISPLAY="Load failed, print stopped"
{% else %}
# The filament the retract was remembered for is gone
# New filament, just purged: a retract remembered for the old one is
# gone. Retract so it does not ooze during the soak; PRINT_START
# undoes it over the bucket.
SAVE_VARIABLE VARIABLE=nozzle_retract VALUE=0
_NOZZLE_RETRACT
M104 S150
{% endif %}
@@ -150,19 +176,37 @@ gcode:
_LIGHTS_ON
M106 S0
# The last homing was before the soak: check that Z still matches the
# Beacon before the gantry goes low. MAX=1.0: the soak itself drifts Z
# a few tenths; the console line shows how much.
_Z_CHECK MAX=1.0
# The bed is soaked now: level and calibrate Z on the final geometry
_STATUS STATE=leveling
RESPOND MSG="quad gantry level"
# Nozzle guard (bd_pressure) on while nothing should touch the nozzle
_GUARD_ON PHASE=qgl
QUAD_GANTRY_LEVEL
# QGL moved the Z steppers and the frame drifted during the soak: home Z
# again (proximity, on the model of the contact homing in PRINT_WARMUP)
# before the brush takes the nozzle below Z0
_GUARD_ON PHASE=z_home
G28 Z
# Own phase: the bristles press on the nozzle too
_GUARD_ON PHASE=brush
BLOBIFIER_CLEAN
_GUARD_OFF
_STATUS STATE=calibrating_z
_NOZZLE_CONTACT_TEMP WAIT=1
G28 Z METHOD=CONTACT CALIBRATE=1 ; calibrate z offset and beacon model hot
_STATUS STATE=meshing
RESPOND MSG="meshing"
_GUARD_ON PHASE=mesh
BED_MESH_CALIBRATE RUNS=2 ; bed mesh in scan mode
# From here on the nozzle extrudes and AFC cuts and loads: guard off
_GUARD_OFF
# Heat over the bucket, then undo the retracts of the previous PRINT_END
_STATUS STATE=heating_extruder
@@ -232,8 +276,9 @@ gcode:
# End-of-print cleanup shared by PRINT_END and CANCEL_PRINT
[gcode_macro _PRINT_CLEANUP]
description: Heaters (incl. chamber heater) and part fan off, flow back to 100%, bdwidth off, mesh and skew cleared
description: Heaters (incl. chamber heater) and part fan off, flow back to 100%, bdwidth off, mesh and skew cleared, nozzle guard off
gcode:
_GUARD_OFF ; a print start that failed while it was armed
TURN_OFF_HEATERS
iHEATER_OFF ; stops the chamber heater control loop too
M107