From 4d506b1afcd8ebdb3ab68f2be7523481ffcf33c0 Mon Sep 17 00:00:00 2001 From: maxabba Date: Mon, 25 Aug 2025 12:22:17 +0200 Subject: [PATCH 1/7] bltouch: add auto z-offset calibration using nozzle probe --- klippy/extras/bltouch.py | 161 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 2bcb9cc10..b64f0f50a 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -41,6 +41,28 @@ 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.) + 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 # output mode omodes = ['5V', 'OD', None] self.output_mode = config.getchoice('set_output_mode', omodes, None) @@ -75,6 +97,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 +308,140 @@ 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") + + toolhead = self.printer.lookup_object('toolhead') + gcode = self.printer.lookup_object('gcode') + heaters = self.printer.lookup_object('heaters') + + # Get optional parameters + 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 + if 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: Probe with BLTouch using double touch for accuracy + gcmd.respond_info("Probing with BLTouch (double touch)...") + self.lower_probe() + self.sync_print_time() + + # First touch at normal speed + from . import probe + phoming = self.printer.lookup_object('homing') + pos = toolhead.get_position() + z_min = 0. + try: + config = self.printer.lookup_object('configfile') + z_config = config.status_raw_config.get('stepper_z', {}) + z_min = float(z_config.get('position_min', 0.)) + except: + z_min = 0. + + pos[2] = z_min + gcmd.respond_info("First touch at %.1f mm/s..." % self.cal_probe_speed) + first_probe = phoming.probing_move(self, pos, self.cal_probe_speed) + + # Retract + toolhead.manual_move([None, None, first_probe[2] + self.cal_retract_dist], self.cal_lift_speed) + + # Second touch at slow speed + gcmd.respond_info("Second touch at %.1f mm/s..." % self.cal_probe_speed_slow) + self.lower_probe() + self.sync_print_time() + pos = toolhead.get_position() + pos[2] = z_min + bltouch_pos = phoming.probing_move(self, pos, self.cal_probe_speed_slow) + + z_bltouch = bltouch_pos[2] + gcmd.respond_info("BLTouch final Z position: %.6f" % z_bltouch) + + # Step 5: Move up and retract BLTouch + toolhead.manual_move([None, None, z_bltouch + 5.], 50.) + self.raise_probe() + self.verify_raise_probe() + + # Step 6: Move nozzle to the exact same XY position where BLTouch touched + # The nozzle should go to the calibration position (cal_x, cal_y) + toolhead.manual_move([cal_x, cal_y, None], 50.) + + # Step 7: Probe with nozzle using double touch for accuracy + gcmd.respond_info("Probing with nozzle (double touch)...") + pos = toolhead.get_position() + + # Calculate safe Z target position for probing + current_z = pos[2] + max_probe_dist = 10.0 # Maximum probing distance + + # Get Z minimum position from stepper_z config or use 0 + try: + config = self.printer.lookup_object('configfile') + z_config = config.status_raw_config.get('stepper_z', {}) + z_min = float(z_config.get('position_min', 0.)) + except: + z_min = 0. + + # Target Z is the maximum between minimum Z and (current - max distance) + target_z = max(z_min, current_z - max_probe_dist) + pos[2] = target_z + + # Setup nozzle endstop for probing + phoming = self.printer.lookup_object('homing') + + # First touch at normal speed + gcmd.respond_info("First nozzle touch at %.1f mm/s..." % self.cal_probe_speed) + first_nozzle = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed) + + # Retract + toolhead.manual_move([None, None, first_nozzle[2] + self.cal_retract_dist], self.cal_lift_speed) + + # Second touch at slow speed + gcmd.respond_info("Second nozzle touch at %.1f mm/s..." % self.cal_probe_speed_slow) + pos = toolhead.get_position() + pos[2] = target_z + nozzle_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) + + z_nozzle = nozzle_pos[2] + gcmd.respond_info("Nozzle final Z position: %.6f" % z_nozzle) + + # Step 8: Calculate and save new z-offset + new_z_offset = z_bltouch - z_nozzle + gcmd.respond_info("Calculated z-offset: %.6f" % new_z_offset) + + # Move up for safety + toolhead.manual_move([None, None, z_nozzle + 10.], 50.) + + # Save the new z-offset + self.position_endstop = new_z_offset + configfile = self.printer.lookup_object('configfile') + configfile.set('bltouch', 'z_offset', "%.6f" % new_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." % new_z_offset) def load_config(config): blt = BLTouchProbe(config) From ee8533fce54deabeceb7ca5c4ccf838280cb53a2 Mon Sep 17 00:00:00 2001 From: maxabba Date: Mon, 25 Aug 2025 14:44:07 +0200 Subject: [PATCH 2/7] bltouch: enhance nozzle probing with configurable retract distance and averaging --- klippy/extras/bltouch.py | 41 +++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index b64f0f50a..754290fbb 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -55,6 +55,8 @@ class BLTouchProbe: 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) else: self.nozzle_endstop = None self.calibration_position = None @@ -63,6 +65,8 @@ class BLTouchProbe: 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 # output mode omodes = ['5V', 'OD', None] self.output_mode = config.getchoice('set_output_mode', omodes, None) @@ -318,6 +322,7 @@ class BLTouchProbe: 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: @@ -336,8 +341,8 @@ class BLTouchProbe: toolhead_y = cal_y - y_offset toolhead.manual_move([toolhead_x, toolhead_y, None], 50.) - # Step 3: Heat nozzle if needed - if nozzle_temp > 0: + # 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) @@ -378,7 +383,7 @@ class BLTouchProbe: gcmd.respond_info("BLTouch final Z position: %.6f" % z_bltouch) # Step 5: Move up and retract BLTouch - toolhead.manual_move([None, None, z_bltouch + 5.], 50.) + toolhead.manual_move([None, None, z_bltouch + 10.], 50.) self.raise_probe() self.verify_raise_probe() @@ -414,16 +419,30 @@ class BLTouchProbe: first_nozzle = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed) # Retract - toolhead.manual_move([None, None, first_nozzle[2] + self.cal_retract_dist], self.cal_lift_speed) + toolhead.manual_move([None, None, first_nozzle[2] + self.cal_nozzle_retract_dist], self.cal_lift_speed) - # Second touch at slow speed - gcmd.respond_info("Second nozzle touch at %.1f mm/s..." % self.cal_probe_speed_slow) - pos = toolhead.get_position() - pos[2] = target_z - nozzle_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) + # Multiple slow speed touches for averaging + nozzle_samples = [] + for i in range(self.cal_nozzle_samples): + gcmd.respond_info("Nozzle touch %d/%d at %.1f mm/s..." % (i+1, self.cal_nozzle_samples, self.cal_probe_speed_slow)) + pos = toolhead.get_position() + pos[2] = target_z + nozzle_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) + nozzle_samples.append(nozzle_pos[2]) + + # Retract between samples if not the last one + if i < self.cal_nozzle_samples - 1: + toolhead.manual_move([None, None, nozzle_pos[2] + self.cal_nozzle_retract_dist], self.cal_lift_speed) - z_nozzle = nozzle_pos[2] - gcmd.respond_info("Nozzle final Z position: %.6f" % z_nozzle) + # Calculate average and standard deviation + z_nozzle = sum(nozzle_samples) / len(nozzle_samples) + if len(nozzle_samples) > 1: + variance = sum((x - z_nozzle) ** 2 for x in nozzle_samples) / len(nozzle_samples) + std_dev = variance ** 0.5 + gcmd.respond_info("Nozzle samples: %s" % ", ".join(["%.6f" % z for z in nozzle_samples])) + gcmd.respond_info("Nozzle average Z: %.6f, StdDev: %.6f" % (z_nozzle, std_dev)) + else: + gcmd.respond_info("Nozzle final Z position: %.6f" % z_nozzle) # Step 8: Calculate and save new z-offset new_z_offset = z_bltouch - z_nozzle From ddef8087f1ddf56c6e1b81894223633af4af1c09 Mon Sep 17 00:00:00 2001 From: maxabba Date: Mon, 25 Aug 2025 16:16:50 +0200 Subject: [PATCH 3/7] bltouch: add nozzle diameter configuration and enhance probing statistics --- klippy/extras/bltouch.py | 88 +++++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 754290fbb..d310ff426 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -57,6 +57,7 @@ class BLTouchProbe: 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 @@ -67,6 +68,7 @@ class BLTouchProbe: 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) @@ -421,27 +423,70 @@ class BLTouchProbe: # Retract toolhead.manual_move([None, None, first_nozzle[2] + self.cal_nozzle_retract_dist], self.cal_lift_speed) - # Multiple slow speed touches for averaging - nozzle_samples = [] - for i in range(self.cal_nozzle_samples): - gcmd.respond_info("Nozzle touch %d/%d at %.1f mm/s..." % (i+1, self.cal_nozzle_samples, self.cal_probe_speed_slow)) - pos = toolhead.get_position() - pos[2] = target_z - nozzle_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) - nozzle_samples.append(nozzle_pos[2]) - - # Retract between samples if not the last one - if i < self.cal_nozzle_samples - 1: - toolhead.manual_move([None, None, nozzle_pos[2] + self.cal_nozzle_retract_dist], self.cal_lift_speed) + # Second touch at slow speed at center position + gcmd.respond_info("Center nozzle touch at %.1f mm/s..." % self.cal_probe_speed_slow) + pos = toolhead.get_position() + pos[2] = target_z + center_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) + center_z = center_pos[2] - # Calculate average and standard deviation - z_nozzle = sum(nozzle_samples) / len(nozzle_samples) + # Retract + toolhead.manual_move([None, None, center_z + self.cal_nozzle_retract_dist], self.cal_lift_speed) + + # Perform cross pattern measurements if enabled + nozzle_samples = [center_z] # Start with center measurement + + if self.cal_nozzle_samples > 1: + # Define cross pattern offsets: back, right, front, left + cross_offsets = [ + (0, -self.cal_nozzle_diameter), # Back + (self.cal_nozzle_diameter, 0), # Right + (0, self.cal_nozzle_diameter), # Front + (-self.cal_nozzle_diameter, 0) # Left + ] + + # Limit to requested number of samples + num_cross_samples = min(self.cal_nozzle_samples - 1, 4) + + for i in range(num_cross_samples): + x_off, y_off = cross_offsets[i] + direction = ["back", "right", "front", "left"][i] + + # Move to cross position + toolhead.manual_move([cal_x + x_off, cal_y + y_off, None], 50.) + + gcmd.respond_info("Cross touch %s (%.1f, %.1f) at %.1f mm/s..." % + (direction, cal_x + x_off, cal_y + y_off, self.cal_probe_speed_slow)) + + pos = toolhead.get_position() + pos[2] = target_z + cross_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) + nozzle_samples.append(cross_pos[2]) + + # Retract + toolhead.manual_move([None, None, cross_pos[2] + self.cal_nozzle_retract_dist], self.cal_lift_speed) + + # Calculate statistics with weighted average favoring center if len(nozzle_samples) > 1: - variance = sum((x - z_nozzle) ** 2 for x in nozzle_samples) / len(nozzle_samples) + # Calculate average and standard deviation of all samples + avg_all = sum(nozzle_samples) / len(nozzle_samples) + variance = sum((x - avg_all) ** 2 for x in nozzle_samples) / len(nozzle_samples) std_dev = variance ** 0.5 - gcmd.respond_info("Nozzle samples: %s" % ", ".join(["%.6f" % z for z in nozzle_samples])) - gcmd.respond_info("Nozzle average Z: %.6f, StdDev: %.6f" % (z_nozzle, std_dev)) + + # Weighted average: center has 50% weight, cross points share remaining 50% + center_weight = 0.5 + cross_weight = 0.5 / (len(nozzle_samples) - 1) + + z_nozzle = center_z * center_weight + for cross_z in nozzle_samples[1:]: + z_nozzle += cross_z * cross_weight + + gcmd.respond_info("Nozzle samples: Center=%.6f, Cross=[%s]" % + (center_z, ", ".join(["%.6f" % z for z in nozzle_samples[1:]]))) + gcmd.respond_info("Center Z: %.6f, Simple avg: %.6f, Weighted avg: %.6f, StdDev: %.6f" % + (center_z, avg_all, z_nozzle, std_dev)) else: + z_nozzle = center_z gcmd.respond_info("Nozzle final Z position: %.6f" % z_nozzle) # Step 8: Calculate and save new z-offset @@ -451,16 +496,19 @@ class BLTouchProbe: # Move up for safety toolhead.manual_move([None, None, z_nozzle + 10.], 50.) + # Convert negative offset to positive for saving + save_z_offset = abs(new_z_offset) if new_z_offset < 0 else new_z_offset + # Save the new z-offset - self.position_endstop = new_z_offset + self.position_endstop = save_z_offset configfile = self.printer.lookup_object('configfile') - configfile.set('bltouch', 'z_offset', "%.6f" % new_z_offset) + 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." % new_z_offset) + "with the above and restart the printer." % save_z_offset) def load_config(config): blt = BLTouchProbe(config) From 318678a468ae3e726b8527657228058579c6e1f0 Mon Sep 17 00:00:00 2001 From: maxabba Date: Wed, 27 Aug 2025 16:34:13 +0200 Subject: [PATCH 4/7] bltouch: add validation for nozzle probe state and z-offset calculation, questa versione "funziona" --- klippy/extras/bltouch.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index d310ff426..e0c83b1b6 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -319,6 +319,12 @@ class BLTouchProbe: 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') @@ -490,14 +496,27 @@ class BLTouchProbe: gcmd.respond_info("Nozzle final Z position: %.6f" % z_nozzle) # Step 8: Calculate and save new z-offset - new_z_offset = z_bltouch - z_nozzle + # z_offset = nozzle_z - bltouch_z (how much lower the nozzle is compared to BLTouch) + # This should always be positive since nozzle touches the bed at a higher Z value + new_z_offset = z_nozzle - z_bltouch + + # 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 higher Z position than the BLTouch.\n" + "Please check your wiring and probe configuration." % + (new_z_offset, z_bltouch, z_nozzle)) + gcmd.respond_info("Calculated z-offset: %.6f" % new_z_offset) + gcmd.respond_info("BLTouch triggers at Z=%.6f, Nozzle touches at Z=%.6f" % (z_bltouch, z_nozzle)) # Move up for safety toolhead.manual_move([None, None, z_nozzle + 10.], 50.) - # Convert negative offset to positive for saving - save_z_offset = abs(new_z_offset) if new_z_offset < 0 else new_z_offset + # Save the positive offset value + save_z_offset = new_z_offset # Save the new z-offset self.position_endstop = save_z_offset From d6d1097da119a1fd540318c85538dcdf4117a536 Mon Sep 17 00:00:00 2001 From: maxabba Date: Wed, 27 Aug 2025 20:04:38 +0200 Subject: [PATCH 5/7] bltouch: refactor probing process to unify BLTouch and nozzle probing methods --- klippy/extras/bltouch.py | 203 +++++++++++++-------------------------- 1 file changed, 69 insertions(+), 134 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index e0c83b1b6..283671301 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -355,165 +355,92 @@ class BLTouchProbe: extruder = toolhead.get_extruder() heaters.set_temperature(extruder.get_heater(), nozzle_temp, True) - # Step 4: Probe with BLTouch using double touch for accuracy - gcmd.respond_info("Probing with BLTouch (double touch)...") - self.lower_probe() - self.sync_print_time() + # Step 4: BLTouch probe - EXACT SAME as PROBE_CALIBRATE + gcmd.respond_info("Performing BLTouch probe (PROBE_CALIBRATE method)...") - # First touch at normal speed + # 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') - pos = toolhead.get_position() - z_min = 0. + + # 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', 0.)) + z_min = float(z_config.get('position_min', -5.0)) except: - z_min = 0. + z_min = -5.0 - pos[2] = z_min - gcmd.respond_info("First touch at %.1f mm/s..." % self.cal_probe_speed) - first_probe = phoming.probing_move(self, pos, self.cal_probe_speed) + # Create probe target position - probe down to find bed + probe_target = [current_pos[0], current_pos[1], z_min] - # Retract - toolhead.manual_move([None, None, first_probe[2] + self.cal_retract_dist], self.cal_lift_speed) + gcmd.respond_info("Nozzle probing from Z=%.3f to Z=%.3f..." % (current_pos[2], z_min)) - # Second touch at slow speed - gcmd.respond_info("Second touch at %.1f mm/s..." % self.cal_probe_speed_slow) - self.lower_probe() - self.sync_print_time() - pos = toolhead.get_position() - pos[2] = z_min - bltouch_pos = phoming.probing_move(self, pos, self.cal_probe_speed_slow) + # 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] - z_bltouch = bltouch_pos[2] - gcmd.respond_info("BLTouch final Z position: %.6f" % z_bltouch) + gcmd.respond_info("Nozzle reference position: %.6f" % z_nozzle_reference) - # Step 5: Move up and retract BLTouch - toolhead.manual_move([None, None, z_bltouch + 10.], 50.) - self.raise_probe() - self.verify_raise_probe() + # 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 - # Step 6: Move nozzle to the exact same XY position where BLTouch touched - # The nozzle should go to the calibration position (cal_x, cal_y) - toolhead.manual_move([cal_x, cal_y, None], 50.) + 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) - # Step 7: Probe with nozzle using double touch for accuracy - gcmd.respond_info("Probing with nozzle (double touch)...") - pos = toolhead.get_position() + # Validate against expected physical measurement + expected_offset = 4.1 # Your measured physical distance + offset_error = abs(new_z_offset - expected_offset) - # Calculate safe Z target position for probing - current_z = pos[2] - max_probe_dist = 10.0 # Maximum probing distance + gcmd.respond_info("Calculated: %.6f, Expected: %.6f, Error: %.6f" % + (new_z_offset, expected_offset, offset_error)) - # Get Z minimum position from stepper_z config or use 0 - try: - config = self.printer.lookup_object('configfile') - z_config = config.status_raw_config.get('stepper_z', {}) - z_min = float(z_config.get('position_min', 0.)) - except: - z_min = 0. - - # Target Z is the maximum between minimum Z and (current - max distance) - target_z = max(z_min, current_z - max_probe_dist) - pos[2] = target_z - - # Setup nozzle endstop for probing - phoming = self.printer.lookup_object('homing') - - # First touch at normal speed - gcmd.respond_info("First nozzle touch at %.1f mm/s..." % self.cal_probe_speed) - first_nozzle = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed) - - # Retract - toolhead.manual_move([None, None, first_nozzle[2] + self.cal_nozzle_retract_dist], self.cal_lift_speed) - - # Second touch at slow speed at center position - gcmd.respond_info("Center nozzle touch at %.1f mm/s..." % self.cal_probe_speed_slow) - pos = toolhead.get_position() - pos[2] = target_z - center_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) - center_z = center_pos[2] - - # Retract - toolhead.manual_move([None, None, center_z + self.cal_nozzle_retract_dist], self.cal_lift_speed) - - # Perform cross pattern measurements if enabled - nozzle_samples = [center_z] # Start with center measurement - - if self.cal_nozzle_samples > 1: - # Define cross pattern offsets: back, right, front, left - cross_offsets = [ - (0, -self.cal_nozzle_diameter), # Back - (self.cal_nozzle_diameter, 0), # Right - (0, self.cal_nozzle_diameter), # Front - (-self.cal_nozzle_diameter, 0) # Left - ] - - # Limit to requested number of samples - num_cross_samples = min(self.cal_nozzle_samples - 1, 4) - - for i in range(num_cross_samples): - x_off, y_off = cross_offsets[i] - direction = ["back", "right", "front", "left"][i] - - # Move to cross position - toolhead.manual_move([cal_x + x_off, cal_y + y_off, None], 50.) - - gcmd.respond_info("Cross touch %s (%.1f, %.1f) at %.1f mm/s..." % - (direction, cal_x + x_off, cal_y + y_off, self.cal_probe_speed_slow)) - - pos = toolhead.get_position() - pos[2] = target_z - cross_pos = phoming.probing_move(self.nozzle_endstop, pos, self.cal_probe_speed_slow) - nozzle_samples.append(cross_pos[2]) - - # Retract - toolhead.manual_move([None, None, cross_pos[2] + self.cal_nozzle_retract_dist], self.cal_lift_speed) - - # Calculate statistics with weighted average favoring center - if len(nozzle_samples) > 1: - # Calculate average and standard deviation of all samples - avg_all = sum(nozzle_samples) / len(nozzle_samples) - variance = sum((x - avg_all) ** 2 for x in nozzle_samples) / len(nozzle_samples) - std_dev = variance ** 0.5 - - # Weighted average: center has 50% weight, cross points share remaining 50% - center_weight = 0.5 - cross_weight = 0.5 / (len(nozzle_samples) - 1) - - z_nozzle = center_z * center_weight - for cross_z in nozzle_samples[1:]: - z_nozzle += cross_z * cross_weight - - gcmd.respond_info("Nozzle samples: Center=%.6f, Cross=[%s]" % - (center_z, ", ".join(["%.6f" % z for z in nozzle_samples[1:]]))) - gcmd.respond_info("Center Z: %.6f, Simple avg: %.6f, Weighted avg: %.6f, StdDev: %.6f" % - (center_z, avg_all, z_nozzle, std_dev)) - else: - z_nozzle = center_z - gcmd.respond_info("Nozzle final Z position: %.6f" % z_nozzle) - - # Step 8: Calculate and save new z-offset - # z_offset = nozzle_z - bltouch_z (how much lower the nozzle is compared to BLTouch) - # This should always be positive since nozzle touches the bed at a higher Z value - new_z_offset = z_nozzle - z_bltouch + 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 higher Z position than the BLTouch.\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, z_nozzle)) - - gcmd.respond_info("Calculated z-offset: %.6f" % new_z_offset) - gcmd.respond_info("BLTouch triggers at Z=%.6f, Nozzle touches at Z=%.6f" % (z_bltouch, z_nozzle)) + (new_z_offset, z_bltouch_reference, z_nozzle_reference)) # Move up for safety - toolhead.manual_move([None, None, z_nozzle + 10.], 50.) + 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 @@ -528,6 +455,14 @@ class BLTouchProbe: "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) From d4b5650157f864f5b09a7e09451cafb74b6e2a32 Mon Sep 17 00:00:00 2001 From: maxabba Date: Wed, 27 Aug 2025 20:41:55 +0200 Subject: [PATCH 6/7] bltouch: Add automatic z-offset calibration using nozzle electrical contact This feature eliminates manual paper testing by using electrical conductivity detection between the nozzle and conductive bed surface. The two-phase calibration process uses BLTouch for reference positioning, then detects nozzle contact at the same coordinates to calculate precise z-offset values. The implementation adds the BLTOUCH_AUTO_Z_OFFSET command with configurable parameters for probe position, speeds, and nozzle temperature. It maintains full backward compatibility as an optional feature. Signed-off-by: Marco Abbattista --- docs/BLTouch.md | 75 ++++++++++++++++++++++++++++++++++++++++ docs/Config_Reference.md | 39 +++++++++++++++++++++ docs/G-Codes.md | 22 ++++++++++++ 3 files changed, 136 insertions(+) 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 83de96096..9a21d1e79 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2181,6 +2181,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 7a60aa8a0..12f6268ab 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. From b8e516e3b5eb67a60498ec1f1e41a7fbf576f4b9 Mon Sep 17 00:00:00 2001 From: maxabba Date: Wed, 27 Aug 2025 20:48:25 +0200 Subject: [PATCH 7/7] bltouch: update copyright information for 2025 --- klippy/extras/bltouch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 283671301..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