diff --git a/klippy/extras/zmax_homing.py b/klippy/extras/zmax_homing.py index b27fdf040..d8666bf99 100644 --- a/klippy/extras/zmax_homing.py +++ b/klippy/extras/zmax_homing.py @@ -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: + 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) \ No newline at end of file