mirror of
https://github.com/Klipper3d/klipper.git
synced 2026-01-11 18:33:11 -07:00
Add Z_CALIBRATE command for automatic Z-axis calibration
Features: - New Z_CALIBRATE command that performs full Z-axis calibration: 1. Executes G28 Z (home to Zmin endstop) 2. Probes upward to find Zmax using Y endstop (SKR3 hardware workaround) 3. Reports actual travel distance vs configured position_max 4. Auto-updates both NanoDLP ZLength and Klipper position_max if diff > 0.5mm 5. Automatically restarts Klipper to load new configuration Technical changes: - Fixed YEndstopWrapper to return Z steppers instead of Y steppers This allows probing_move() to correctly track Z-axis movement while using Y endstop for trigger detection - Temporarily expands Z limits during calibration to allow full travel - Updates NanoDLP machine.json CustomValues.ZLength directly (JSON modification) - Updates Klipper printer.cfg position_max via regex replacement Usage: Z_CALIBRATE # Use default speed Z_CALIBRATE SPEED=5 # Custom probe speed (mm/s) Z_CALIBRATE MAX_TRAVEL=200 # Custom max travel distance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0348d38126
commit
cdb798aee5
1 changed files with 265 additions and 17 deletions
|
|
@ -7,17 +7,28 @@
|
|||
import logging
|
||||
|
||||
class YEndstopWrapper:
|
||||
"""Wrapper para el endstop de Y que es compatible con probing_move()"""
|
||||
def __init__(self, y_endstop):
|
||||
"""Wrapper para el endstop de Y que es compatible con probing_move()
|
||||
|
||||
Usa el endstop de Y para la detección, pero devuelve los steppers de Z
|
||||
para que probing_move mida correctamente el movimiento del eje Z.
|
||||
"""
|
||||
def __init__(self, y_endstop, z_steppers):
|
||||
self.y_endstop = y_endstop
|
||||
self.z_steppers = z_steppers
|
||||
# Delegar métodos del endstop original
|
||||
self.get_mcu = y_endstop.get_mcu
|
||||
self.add_stepper = y_endstop.add_stepper
|
||||
self.get_steppers = y_endstop.get_steppers
|
||||
self.home_start = y_endstop.home_start
|
||||
self.home_wait = y_endstop.home_wait
|
||||
self.query_endstop = y_endstop.query_endstop
|
||||
|
||||
def get_steppers(self):
|
||||
# Devolver steppers de Z para que probing_move mida el eje correcto
|
||||
return self.z_steppers
|
||||
|
||||
def add_stepper(self, stepper):
|
||||
# No añadir steppers al endstop Y original
|
||||
pass
|
||||
|
||||
class ZMaxHomingAlt:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
|
|
@ -46,7 +57,12 @@ class ZMaxHomingAlt:
|
|||
self.cmd_ZMAX_HOME,
|
||||
desc=self.cmd_ZMAX_HOME_help
|
||||
)
|
||||
|
||||
self.gcode.register_command(
|
||||
'Z_CALIBRATE',
|
||||
self.cmd_Z_CALIBRATE,
|
||||
desc=self.cmd_Z_CALIBRATE_help
|
||||
)
|
||||
|
||||
logging.info(f"ZMaxHoming: Initialized with speeds - homing:{self.speed} mm/s, second:{self.second_speed} mm/s")
|
||||
|
||||
def _handle_connect(self):
|
||||
|
|
@ -54,31 +70,37 @@ class ZMaxHomingAlt:
|
|||
self.toolhead = self.printer.lookup_object('toolhead')
|
||||
self.phoming = self.printer.lookup_object('homing')
|
||||
|
||||
# Obtener el endstop de Y usando la forma estándar
|
||||
# Obtener referencias de la cinemática
|
||||
kin = self.toolhead.get_kinematics()
|
||||
z_steppers = None
|
||||
|
||||
if hasattr(kin, 'rails'):
|
||||
# Primero obtener el stepper Z
|
||||
for rail in kin.rails:
|
||||
if rail.get_name() == "stepper_z":
|
||||
self.z_stepper = rail
|
||||
z_steppers = rail.get_steppers()
|
||||
z_max_cfg = rail.get_range()[1]
|
||||
logging.info(f"ZMaxHoming: Z-max position from config: {z_max_cfg}mm")
|
||||
logging.info(f"ZMaxHoming: Z steppers: {[s.get_name() for s in z_steppers]}")
|
||||
break
|
||||
|
||||
# Obtener el endstop de Y
|
||||
for rail in kin.rails:
|
||||
if rail.get_name() == "stepper_y":
|
||||
endstops = rail.get_endstops()
|
||||
if endstops:
|
||||
self.y_endstop = endstops[0][0]
|
||||
# Crear wrapper compatible con probing_move()
|
||||
self.y_endstop_wrapper = YEndstopWrapper(self.y_endstop)
|
||||
logging.info(f"ZMaxHoming: Using Y endstop for Z-max homing")
|
||||
break
|
||||
|
||||
# Obtener el stepper Z
|
||||
for rail in kin.rails:
|
||||
if rail.get_name() == "stepper_z":
|
||||
self.z_stepper = rail
|
||||
z_max_cfg = self.z_stepper.get_range()[1]
|
||||
logging.info(f"ZMaxHoming: Z-max position from config: {z_max_cfg}mm")
|
||||
break
|
||||
|
||||
|
||||
if self.y_endstop is None:
|
||||
raise self.printer.config_error("No se encontró el endstop de Y")
|
||||
if self.z_stepper is None:
|
||||
raise self.printer.config_error("No se encontró el stepper Z")
|
||||
|
||||
# Crear wrapper que usa endstop Y pero mide con steppers Z
|
||||
self.y_endstop_wrapper = YEndstopWrapper(self.y_endstop, z_steppers)
|
||||
|
||||
def _continuous_probe_move(self, target_pos, speed):
|
||||
"""
|
||||
|
|
@ -318,5 +340,231 @@ class ZMaxHomingAlt:
|
|||
if not gcode_state:
|
||||
self.gcode.run_script_from_command("G91")
|
||||
|
||||
cmd_Z_CALIBRATE_help = "Calibra el eje Z: hace G28 Z, sube hasta Zmax (endstop Y) y reporta la distancia real recorrida."
|
||||
def cmd_Z_CALIBRATE(self, gcmd):
|
||||
"""
|
||||
Comando de calibración del eje Z:
|
||||
1. Ejecuta G28 Z (homing normal hacia abajo con endstop Zmin)
|
||||
2. Sube buscando el endstop Y (Zmax) sin perder la posición
|
||||
3. Reporta la distancia real recorrida
|
||||
"""
|
||||
if not all([self.toolhead, self.y_endstop_wrapper, self.z_stepper, self.phoming]):
|
||||
raise gcmd.error("Z_CALIBRATE: Sistema no inicializado correctamente")
|
||||
|
||||
# Parámetros opcionales
|
||||
speed = gcmd.get_float('SPEED', self.speed, above=0.)
|
||||
# Límite de seguridad: cuánto subir como máximo buscando el endstop
|
||||
max_travel = gcmd.get_float('MAX_TRAVEL', 150.0, above=0.)
|
||||
|
||||
gcmd.respond_info("=== Z_CALIBRATE: Iniciando calibración del eje Z ===")
|
||||
|
||||
# Guardar estado de coordenadas
|
||||
gcode_move = self.printer.lookup_object('gcode_move')
|
||||
gcode_state = gcode_move.get_status()['absolute_coordinates']
|
||||
self.gcode.run_script_from_command("G90")
|
||||
|
||||
try:
|
||||
# --- PASO 1: Hacer homing normal hacia abajo (G28 Z) ---
|
||||
gcmd.respond_info("Paso 1: Ejecutando G28 Z (homing hacia Zmin)...")
|
||||
self.gcode.run_script_from_command("G28 Z")
|
||||
self.toolhead.wait_moves()
|
||||
|
||||
# Obtener posición después del homing (debería ser position_endstop)
|
||||
pos_after_homing = self.toolhead.get_position()
|
||||
z_start = pos_after_homing[2]
|
||||
gcmd.respond_info(f" Posición después de G28 Z: {z_start:.3f}mm")
|
||||
|
||||
# --- PASO 2: Subir buscando el endstop Y (Zmax) ---
|
||||
gcmd.respond_info(f"Paso 2: Subiendo hasta encontrar endstop Zmax (max {max_travel:.1f}mm)...")
|
||||
|
||||
# Verificar si el endstop Y ya está activado (no debería estarlo abajo)
|
||||
self.toolhead.wait_moves()
|
||||
query_time = self.toolhead.get_last_move_time()
|
||||
if self.y_endstop.query_endstop(query_time):
|
||||
gcmd.respond_info(" ADVERTENCIA: Endstop Y ya activado en posición inicial!")
|
||||
gcmd.respond_info(" Esto indica un problema de cableado o configuración.")
|
||||
return
|
||||
|
||||
# Expandir temporalmente los límites de Z para permitir el movimiento
|
||||
# Guardamos los límites originales para restaurarlos después
|
||||
kin = self.toolhead.get_kinematics()
|
||||
original_z_limits = kin.limits[2] # (min, max) para eje Z
|
||||
|
||||
# Establecer nuevos límites temporales: desde posición actual hasta max_travel
|
||||
current_pos = self.toolhead.get_position()
|
||||
temp_z_max = current_pos[2] + max_travel
|
||||
kin.update_limits(2, (original_z_limits[0], temp_z_max))
|
||||
gcmd.respond_info(f" Límites Z temporales: {original_z_limits[0]:.1f} - {temp_z_max:.1f}mm")
|
||||
|
||||
# Calcular posición objetivo
|
||||
target_pos = list(current_pos)
|
||||
target_pos[2] = temp_z_max
|
||||
|
||||
# Usar probing_move para subir hasta que se active el endstop Y
|
||||
probing_error = None
|
||||
try:
|
||||
trigger_pos = self.phoming.probing_move(
|
||||
self.y_endstop_wrapper,
|
||||
target_pos,
|
||||
speed
|
||||
)
|
||||
# La distancia recorrida es la diferencia entre donde activó y donde empezó
|
||||
z_end = trigger_pos[2]
|
||||
travel_distance = z_end - z_start
|
||||
gcmd.respond_info(f" Endstop Y activado en Z={z_end:.3f}mm (recorrido: {travel_distance:.3f}mm)")
|
||||
|
||||
except self.printer.command_error as e:
|
||||
if "No trigger" in str(e):
|
||||
probing_error = gcmd.error(
|
||||
f"El endstop Y (Zmax) no se activó después de {max_travel:.1f}mm. "
|
||||
"Aumenta MAX_TRAVEL o verifica el endstop."
|
||||
)
|
||||
elif "Probe triggered prior to movement" in str(e):
|
||||
# El endstop se activó inmediatamente
|
||||
travel_distance = 0.0
|
||||
z_end = z_start
|
||||
gcmd.respond_info(f" Endstop Y activado inmediatamente en Z={z_end:.3f}mm")
|
||||
else:
|
||||
probing_error = e
|
||||
|
||||
# Restaurar límites originales de Z (siempre)
|
||||
kin.update_limits(2, original_z_limits)
|
||||
|
||||
# Si hubo error, lanzarlo ahora
|
||||
if probing_error:
|
||||
raise probing_error
|
||||
|
||||
# --- PASO 3: Calcular y reportar resultados ---
|
||||
|
||||
gcmd.respond_info("")
|
||||
gcmd.respond_info("=== RESULTADOS DE CALIBRACIÓN ===")
|
||||
gcmd.respond_info(f" Posición inicial (Zmin): {z_start:.3f}mm")
|
||||
gcmd.respond_info(f" Posición final (Zmax): {z_end:.3f}mm")
|
||||
gcmd.respond_info(f" ----------------------------------------")
|
||||
gcmd.respond_info(f" DISTANCIA RECORRIDA: {travel_distance:.3f}mm")
|
||||
gcmd.respond_info("")
|
||||
|
||||
# Obtener configuración actual para comparar
|
||||
z_min_cfg, z_max_cfg = self.z_stepper.get_range()
|
||||
config_range = z_max_cfg - z_min_cfg
|
||||
|
||||
gcmd.respond_info(f" Configuración actual:")
|
||||
gcmd.respond_info(f" position_min: {z_min_cfg:.3f}mm")
|
||||
gcmd.respond_info(f" position_max: {z_max_cfg:.3f}mm")
|
||||
gcmd.respond_info(f" Rango config: {config_range:.3f}mm")
|
||||
gcmd.respond_info("")
|
||||
|
||||
# Sugerencia de nuevo position_max
|
||||
# El nuevo position_max debería ser: position_endstop + distancia_recorrida
|
||||
# Pero position_endstop ya está incluido en z_start después del G28
|
||||
suggested_max = z_start + travel_distance
|
||||
|
||||
diff = suggested_max - z_max_cfg
|
||||
if abs(diff) > 0.5:
|
||||
gcmd.respond_info(f" Diferencia con config actual: {diff:+.3f}mm")
|
||||
gcmd.respond_info("")
|
||||
gcmd.respond_info("=== ACTUALIZANDO CONFIGURACIÓN ===")
|
||||
|
||||
# --- Actualizar NanoDLP CustomValues.ZLength ---
|
||||
nanodlp_updated = self._update_nanodlp_zlength(gcmd, suggested_max)
|
||||
|
||||
# --- Actualizar Klipper printer.cfg position_max ---
|
||||
klipper_updated = self._update_klipper_position_max(gcmd, suggested_max)
|
||||
|
||||
if klipper_updated:
|
||||
gcmd.respond_info("")
|
||||
gcmd.respond_info("=== REINICIANDO KLIPPER ===")
|
||||
gcmd.respond_info(" La nueva configuración se cargará automáticamente...")
|
||||
# Programar reinicio después de responder
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.register_callback(lambda e: self._restart_klipper())
|
||||
elif not nanodlp_updated and not klipper_updated:
|
||||
gcmd.respond_info("")
|
||||
gcmd.respond_info(" ⚠ No se pudo actualizar ninguna configuración")
|
||||
else:
|
||||
gcmd.respond_info(f" La configuración actual es correcta (diff: {diff:+.3f}mm)")
|
||||
gcmd.respond_info(" No se requieren cambios.")
|
||||
|
||||
finally:
|
||||
# Restaurar estado Gcode
|
||||
if not gcode_state:
|
||||
self.gcode.run_script_from_command("G91")
|
||||
|
||||
def _update_nanodlp_zlength(self, gcmd, new_zlength):
|
||||
"""Actualiza CustomValues.ZLength en NanoDLP modificando machine.json directamente"""
|
||||
import json
|
||||
|
||||
machine_json_path = "/home/pi/nanodlp/db/machine.json"
|
||||
gcmd.respond_info(f" Actualizando NanoDLP ZLength a {new_zlength:.3f}mm...")
|
||||
|
||||
try:
|
||||
# Leer archivo JSON actual
|
||||
with open(machine_json_path, 'r') as f:
|
||||
machine_config = json.load(f)
|
||||
|
||||
# Asegurar que CustomValues existe
|
||||
if 'CustomValues' not in machine_config:
|
||||
machine_config['CustomValues'] = {}
|
||||
|
||||
# Actualizar ZLength (como string, igual que los demás valores)
|
||||
old_value = machine_config['CustomValues'].get('ZLength', 'N/A')
|
||||
machine_config['CustomValues']['ZLength'] = f"{new_zlength:.3f}"
|
||||
|
||||
# Escribir archivo actualizado
|
||||
with open(machine_json_path, 'w') as f:
|
||||
json.dump(machine_config, f, indent='\t')
|
||||
|
||||
gcmd.respond_info(f" ✓ NanoDLP ZLength actualizado: {old_value} → {new_zlength:.3f}mm")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
gcmd.respond_info(f" ✗ Error actualizando NanoDLP: {e}")
|
||||
return False
|
||||
|
||||
def _update_klipper_position_max(self, gcmd, new_position_max):
|
||||
"""Actualiza position_max en printer.cfg"""
|
||||
import re
|
||||
|
||||
config_path = "/home/pi/printer_data/config/printer.cfg"
|
||||
gcmd.respond_info(f" Actualizando Klipper position_max a {new_position_max:.3f}mm...")
|
||||
|
||||
try:
|
||||
# Leer archivo actual
|
||||
with open(config_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Buscar y reemplazar position_max en la sección [stepper_z]
|
||||
# Patrón: position_max: <número>
|
||||
pattern = r'(\[stepper_z\].*?position_max\s*:\s*)(\d+\.?\d*)'
|
||||
|
||||
def replacer(match):
|
||||
return f"{match.group(1)}{new_position_max:.3f}"
|
||||
|
||||
new_content, count = re.subn(pattern, replacer, content, count=1, flags=re.DOTALL)
|
||||
|
||||
if count == 0:
|
||||
gcmd.respond_info(f" ✗ No se encontró position_max en [stepper_z]")
|
||||
return False
|
||||
|
||||
# Escribir archivo actualizado
|
||||
with open(config_path, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
gcmd.respond_info(f" ✓ Klipper position_max actualizado correctamente")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
gcmd.respond_info(f" ✗ Error actualizando printer.cfg: {e}")
|
||||
return False
|
||||
|
||||
def _restart_klipper(self):
|
||||
"""Reinicia Klipper para cargar la nueva configuración"""
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(['sudo', 'systemctl', 'restart', 'klipper.service'],
|
||||
timeout=5, capture_output=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error reiniciando Klipper: {e}")
|
||||
|
||||
def load_config(config):
|
||||
return ZMaxHomingAlt(config)
|
||||
Loading…
Add table
Add a link
Reference in a new issue