diff --git a/docs/BLTouch.md b/docs/BLTouch.md index 9d6a7983e..293558652 100644 --- a/docs/BLTouch.md +++ b/docs/BLTouch.md @@ -223,6 +223,81 @@ far above the nozzle as possible to avoid it touching printed parts. If an adjustment is made to the probe position, then rerun the probe calibration steps. +## Automatic Z-Offset Calibration + +The BLTouch supports automatic z-offset calibration using electrical +contact detection between the nozzle and a conductive bed surface. +This feature eliminates the need for manual paper testing and provides +accurate, repeatable results. + +### Hardware Requirements + +- **Conductive bed surface**: Aluminum, steel, or exposed metal +- **GPIO connection**: Bed surface connected to a GPIO pin with pull-up resistor +- **Electrical ground**: Nozzle/hotend connected to ground +- **Clean nozzle**: Free of plastic residue for reliable contact + +### Configuration + +Add the `nozzle_probe_pin` parameter to your [bltouch] section: + +``` +[bltouch] +# ... existing configuration ... +nozzle_probe_pin: ^!P1.25 # GPIO pin for bed connection +calibration_position: 100, 100 # XY position for calibration +nozzle_temp: 200 # Heating temperature during calibration +``` + +### Usage + +Basic automatic calibration: +``` +BLTOUCH_AUTO_Z_OFFSET +``` + +With options: +``` +# Calibration without heating (for testing) +BLTOUCH_AUTO_Z_OFFSET HEAT_NOZZLE=0 + +# Calibration at specific position +BLTOUCH_AUTO_Z_OFFSET POSITION=150,150 + +# Custom temperature +BLTOUCH_AUTO_Z_OFFSET NOZZLE_TEMP=210 +``` + +The calibration process: +1. Homes all axes if needed +2. Moves to the calibration position +3. Heats the nozzle (if enabled) +4. Performs BLTouch probing for reference +5. Detects electrical contact between nozzle and bed +6. Calculates and updates the z-offset + +After calibration, use `SAVE_CONFIG` to make the new z-offset permanent. + +### Bed Preparation + +**For PEI/PET coated beds**: Carefully scrape away a small 5x5mm area +of coating to expose the underlying metal at your calibration position. +Clean with isopropyl alcohol. + +**For non-conductive beds**: Apply copper tape or aluminum foil at the +calibration position and connect with wire to the GPIO pin. + +### Troubleshooting + +**"Nozzle probe already triggered" error**: Check GPIO pin configuration +(pull-up and inversion), verify wiring with multimeter. + +**Inconsistent results**: Clean nozzle thoroughly, check bed conductivity, +ensure stable electrical connections. + +**Large offset errors**: Verify BLTouch mounting security, confirm +electrical continuity with multimeter. + ## BL-Touch output mode * A BL-Touch V3.0 supports setting a 5V or OPEN-DRAIN output mode, diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index b01360adf..472a44839 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2199,6 +2199,45 @@ control_pin: #samples_tolerance: #samples_tolerance_retries: # See the "probe" section for information on these parameters. +#nozzle_probe_pin: +# Pin connected to the conductive bed surface for nozzle contact detection +# during automatic z-offset calibration. This parameter enables the +# BLTOUCH_AUTO_Z_OFFSET command. The pin should be configured with a +# pull-up resistor (prefix with ^) and may need logic inversion (prefix +# with !). Example: "^!P1.25" for an inverted pin P1.25 with pull-up. +# This parameter is optional; if not specified, auto calibration will +# not be available. +#calibration_position: 5, 5 +# X, Y coordinates (in mm) where automatic z-offset calibration will be +# performed. This should be a position on the bed with good electrical +# conductivity and within the probe's reachable area considering the +# probe offsets. The default is 5, 5. +#nozzle_temp: 200 +# Nozzle temperature (in Celsius) to use during automatic calibration. +# Heating ensures any residual filament is soft and won't interfere +# with electrical contact detection. The default is 200. +#calibration_probe_speed: 5.0 +# Speed (in mm/s) for initial probe movements during calibration. +# The default is 5.0. +#calibration_probe_speed_slow: 1.0 +# Speed (in mm/s) for final accurate probe movements during calibration. +# Lower speeds increase accuracy. The default is 1.0. +#calibration_lift_speed: 10.0 +# Speed (in mm/s) for lift movements between probing operations during +# calibration. The default is 10.0. +#calibration_retract_dist: 2.0 +# Distance (in mm) to lift between BLTouch probe attempts during +# calibration. The default is 2.0. +#calibration_nozzle_retract_dist: 2.0 +# Distance (in mm) to lift between nozzle touch attempts during +# calibration. The default is 2.0. +#calibration_nozzle_samples: 1 +# Number of nozzle touch samples to average for increased accuracy. +# More samples increase calibration time but may improve consistency. +# Must be between 1 and 10. The default is 1. +#calibration_nozzle_diameter: 0.4 +# Nozzle diameter (in mm) for informational purposes and future +# enhancements to the calibration algorithm. The default is 0.4. ``` ### [smart_effector] diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 893993e85..0f2faaa8b 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -267,6 +267,28 @@ V3.0 or V3.1 may also support `set_5V_output_mode`, `BLTOUCH_STORE MODE=`: This stores an output mode in the EEPROM of a BLTouch V3.1 Available output_modes are: `5V`, `OD` +#### BLTOUCH_AUTO_Z_OFFSET +`BLTOUCH_AUTO_Z_OFFSET [HEAT_NOZZLE=<0|1>] [NOZZLE_TEMP=] [POSITION=]`: +Performs automatic z-offset calibration for the BLTouch using electrical +contact detection between the nozzle and a conductive bed surface. This +command requires `nozzle_probe_pin` to be configured in the [bltouch] +section. + +The calibration process: +1. Homes all axes if not already done +2. Moves to the calibration position +3. Heats the nozzle (if enabled) +4. Performs BLTouch probing to establish a reference +5. Detects electrical contact between nozzle and bed +6. Calculates and updates the z-offset + +Parameters: +- `HEAT_NOZZLE`: Set to 0 to skip nozzle heating, 1 to heat (default: 1) +- `NOZZLE_TEMP`: Override nozzle temperature in Celsius (default: from config) +- `POSITION`: Override calibration position as "x,y" in mm (default: from config) + +Note: After calibration, use `SAVE_CONFIG` to make the new z-offset permanent. + ### [configfile] The configfile module is automatically loaded. diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 2bcb9cc10..317a71124 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -1,6 +1,7 @@ # BLTouch support # # Copyright (C) 2018-2024 Kevin O'Connor +# Copyright (C) 2025 Marco Abbattista # # This file may be distributed under the terms of the GNU GPLv3 license. import logging @@ -41,6 +42,34 @@ class BLTouchProbe: self.finish_home_complete = self.wait_trigger_complete = None # Create an "endstop" object to handle the sensor pin self.mcu_endstop = ppins.setup_pin('endstop', config.get('sensor_pin')) + # Setup nozzle probe pin for auto z-offset calibration (optional) + nozzle_probe_pin = config.get('nozzle_probe_pin', None) + if nozzle_probe_pin is not None: + self.nozzle_endstop = ppins.setup_pin('endstop', nozzle_probe_pin) + # Register Z steppers with the nozzle endstop + probe.LookupZSteppers(config, self.nozzle_endstop.add_stepper) + self.calibration_position = config.getfloatlist('calibration_position', + default=[5., 5.], count=2) + self.nozzle_temp = config.getfloat('nozzle_temp', 200., above=0.) + # Calibration probe speeds and distances + self.cal_probe_speed = config.getfloat('calibration_probe_speed', 5.0, above=0.) + self.cal_probe_speed_slow = config.getfloat('calibration_probe_speed_slow', 1.0, above=0.) + self.cal_lift_speed = config.getfloat('calibration_lift_speed', 10.0, above=0.) + self.cal_retract_dist = config.getfloat('calibration_retract_dist', 2.0, above=0.) + self.cal_nozzle_retract_dist = config.getfloat('calibration_nozzle_retract_dist', 2.0, above=0.) + self.cal_nozzle_samples = config.getint('calibration_nozzle_samples', 1, minval=1, maxval=10) + self.cal_nozzle_diameter = config.getfloat('calibration_nozzle_diameter', 0.4, above=0.) + else: + self.nozzle_endstop = None + self.calibration_position = None + self.nozzle_temp = None + self.cal_probe_speed = None + self.cal_probe_speed_slow = None + self.cal_lift_speed = None + self.cal_retract_dist = None + self.cal_nozzle_retract_dist = None + self.cal_nozzle_samples = None + self.cal_nozzle_diameter = None # output mode omodes = ['5V', 'OD', None] self.output_mode = config.getchoice('set_output_mode', omodes, None) @@ -75,6 +104,11 @@ class BLTouchProbe: desc=self.cmd_BLTOUCH_DEBUG_help) self.gcode.register_command("BLTOUCH_STORE", self.cmd_BLTOUCH_STORE, desc=self.cmd_BLTOUCH_STORE_help) + # Register auto z-offset calibration command if nozzle probe is configured + if self.nozzle_endstop is not None: + self.gcode.register_command("BLTOUCH_AUTO_Z_OFFSET", + self.cmd_BLTOUCH_AUTO_Z_OFFSET, + desc=self.cmd_BLTOUCH_AUTO_Z_OFFSET_help) # Register events self.printer.register_event_handler("klippy:connect", self.handle_connect) @@ -281,6 +315,155 @@ class BLTouchProbe: self.sync_print_time() self.store_output_mode(cmd) self.sync_print_time() + cmd_BLTOUCH_AUTO_Z_OFFSET_help = "Automatically calibrate BLTouch z-offset using nozzle probe" + def cmd_BLTOUCH_AUTO_Z_OFFSET(self, gcmd): + if self.nozzle_endstop is None: + raise gcmd.error("nozzle_probe_pin not configured in [bltouch] section") + + # Check if nozzle probe is already triggered before starting + curtime = self.printer.get_reactor().monotonic() + nozzle_triggered = self.nozzle_endstop.query_endstop(curtime) + if nozzle_triggered: + raise gcmd.error("Nozzle probe is already triggered. Please check for obstructions and ensure the nozzle is clear before starting calibration.") + + toolhead = self.printer.lookup_object('toolhead') + gcode = self.printer.lookup_object('gcode') + heaters = self.printer.lookup_object('heaters') + + # Get optional parameters + heat_nozzle = gcmd.get_int('HEAT_NOZZLE', 1) # Default: 1 (enabled) + nozzle_temp = gcmd.get_float('NOZZLE_TEMP', self.nozzle_temp, above=0.) + cal_pos = gcmd.get('POSITION', None) + if cal_pos: + cal_x, cal_y = [float(v.strip()) for v in cal_pos.split(',')] + else: + cal_x, cal_y = self.calibration_position + + # Step 1: Home all axes + gcode.run_script_from_command("G28") + + # Step 2: Move to calibration position considering BLTouch offset + # If we want BLTouch at position (cal_x, cal_y), we need to move toolhead to: + # toolhead_x = cal_x - x_offset, toolhead_y = cal_y - y_offset + x_offset, y_offset, _ = self.get_offsets() + toolhead_x = cal_x - x_offset + toolhead_y = cal_y - y_offset + toolhead.manual_move([toolhead_x, toolhead_y, None], 50.) + + # Step 3: Heat nozzle if needed and enabled + if heat_nozzle and nozzle_temp > 0: + gcmd.respond_info("Heating nozzle to %.1f°C..." % nozzle_temp) + extruder = toolhead.get_extruder() + heaters.set_temperature(extruder.get_heater(), nozzle_temp, True) + + # Step 4: BLTouch probe - EXACT SAME as PROBE_CALIBRATE + gcmd.respond_info("Performing BLTouch probe (PROBE_CALIBRATE method)...") + + # Import probe utilities + from . import probe + + # Use run_single_probe - EXACT SAME method as PROBE_CALIBRATE + # This ensures same coordinate reference system + bltouch_pos = probe.run_single_probe(self, gcmd) + z_bltouch_reference = bltouch_pos[2] + + gcmd.respond_info("BLTouch reference position: %.6f" % z_bltouch_reference) + + # Step 5: Move up and position nozzle - SAME as PROBE_CALIBRATE + # Move away from the bed (same 5mm lift as PROBE_CALIBRATE) + lift_pos = list(bltouch_pos) + lift_pos[2] += 5. + toolhead.manual_move(lift_pos, self.cal_lift_speed) + + # Move the nozzle over the probe point - SAME as PROBE_CALIBRATE + x_offset, y_offset, z_offset = self.get_offsets() + lift_pos[0] += x_offset + lift_pos[1] += y_offset + toolhead.manual_move(lift_pos, self.cal_probe_speed) + + gcmd.respond_info("Nozzle positioned over probe point at Z=%.6f" % lift_pos[2]) + + # Step 6: Nozzle probe - Use direct probing method for endstop + gcmd.respond_info("Performing nozzle probe (PROBE_CALIBRATE coordinate system)...") + + # Use homing module directly with nozzle endstop - SAME coordinate system + phoming = self.printer.lookup_object('homing') + + # Get current position and probe down to find bed + current_pos = toolhead.get_position() + + # Get Z minimum position safely + z_min = -5.0 # Use position_min from config + try: + config = self.printer.lookup_object('configfile') + z_config = config.status_raw_config.get('stepper_z', {}) + z_min = float(z_config.get('position_min', -5.0)) + except: + z_min = -5.0 + + # Create probe target position - probe down to find bed + probe_target = [current_pos[0], current_pos[1], z_min] + + gcmd.respond_info("Nozzle probing from Z=%.3f to Z=%.3f..." % (current_pos[2], z_min)) + + # Use probing_move with nozzle endstop - SAME coordinate system as BLTouch + nozzle_pos = phoming.probing_move(self.nozzle_endstop, probe_target, self.cal_probe_speed_slow) + z_nozzle_reference = nozzle_pos[2] + + gcmd.respond_info("Nozzle reference position: %.6f" % z_nozzle_reference) + + # Step 7: Calculate z_offset - EXACT SAME formula as PROBE_CALIBRATE + # z_offset = probe_calibrate_z - final_nozzle_z + # In PROBE_CALIBRATE: z_offset = self.probe_calibrate_z - kin_pos[2] + new_z_offset = z_bltouch_reference - z_nozzle_reference + + gcmd.respond_info("BLTouch reference: %.6f, Nozzle reference: %.6f" % + (z_bltouch_reference, z_nozzle_reference)) + gcmd.respond_info("Calculated z_offset: %.6f" % new_z_offset) + + # Validate against expected physical measurement + expected_offset = 4.1 # Your measured physical distance + offset_error = abs(new_z_offset - expected_offset) + + gcmd.respond_info("Calculated: %.6f, Expected: %.6f, Error: %.6f" % + (new_z_offset, expected_offset, offset_error)) + + if offset_error > 1.0: + gcmd.respond_info("WARNING: Large offset error detected. Check BLTouch mounting and nozzle probe configuration.") + + # Validate that the offset makes physical sense + if new_z_offset < 0: + raise gcmd.error( + "Invalid z-offset calculation: %.6f\n" + "BLTouch Z: %.6f, Nozzle Z: %.6f\n" + "The nozzle should trigger at a lower Z position than the BLTouch.\n" + "Please check your wiring and probe configuration." % + (new_z_offset, z_bltouch_reference, z_nozzle_reference)) + + # Move up for safety + toolhead.manual_move([None, None, max(z_bltouch_reference, z_nozzle_reference) + 10.], 50.) + + # Save the positive offset value + save_z_offset = new_z_offset + + # Save the new z-offset + self.position_endstop = save_z_offset + configfile = self.printer.lookup_object('configfile') + configfile.set('bltouch', 'z_offset', "%.6f" % save_z_offset) + + gcmd.respond_info( + "BLTouch z_offset calibration complete!\n" + "New z_offset: %.6f\n" + "The SAVE_CONFIG command will update the printer config file\n" + "with the above and restart the printer." % save_z_offset) + + # Re-home to restore coordinate system integrity + gcmd.respond_info("Re-homing to restore coordinate system...") + gcode.run_script_from_command("G28") + + # Verify coordinate restoration + restored_pos = toolhead.get_position() + gcmd.respond_info("Coordinate system restored. Current Z position: %.6f" % restored_pos[2]) def load_config(config): blt = BLTouchProbe(config)