diff --git a/kiauh/extensions/tmc_autotune/__init__.py b/kiauh/extensions/tmc_autotune/__init__.py new file mode 100644 index 0000000..9438829 --- /dev/null +++ b/kiauh/extensions/tmc_autotune/__init__.py @@ -0,0 +1,28 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2026 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # +from pathlib import Path + +# repo +TMCA_REPO = "https://github.com/andrewmcgr/klipper_tmc_autotune" + +# directories +TMCA_DIR = Path.home().joinpath("klipper_tmc_autotune") +MODULE_PATH = Path(__file__).resolve().parent +KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") +KLIPPER_PLUGINS = KLIPPER_DIR.joinpath("klippy/plugins") +KLIPPER_EXTENSIONS_PATH = ( + KLIPPER_PLUGINS if KLIPPER_PLUGINS.is_dir() else KLIPPER_EXTRAS +) + +# files +TMCA_EXAMPLE_CONFIG = TMCA_DIR.joinpath("docs/example.cfg") + +# names +TMCA_MOONRAKER_UPDATER_NAME = "update_manager klipper_tmc_autotune" diff --git a/kiauh/extensions/tmc_autotune/metadata.json b/kiauh/extensions/tmc_autotune/metadata.json new file mode 100644 index 0000000..87f9bd1 --- /dev/null +++ b/kiauh/extensions/tmc_autotune/metadata.json @@ -0,0 +1,13 @@ +{ + "metadata": { + "index": 13, + "module": "tmc_autotune_extension", + "maintained_by": "theogayar", + "display_name": "Klipper TMC Autotune", + "description": [ + "Klipper extension for automatic configuration and tuning of TMC drivers." + ], + "repo": "https://github.com/andrewmcgr/klipper_tmc_autotune", + "updates": true + } +} diff --git a/kiauh/extensions/tmc_autotune/tmc_autotune_extension.py b/kiauh/extensions/tmc_autotune/tmc_autotune_extension.py new file mode 100644 index 0000000..de9621a --- /dev/null +++ b/kiauh/extensions/tmc_autotune/tmc_autotune_extension.py @@ -0,0 +1,384 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2026 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # +import shutil +from typing import List + +from components.klipper.klipper import Klipper +from components.moonraker.moonraker import Moonraker +from core.instance_manager.instance_manager import InstanceManager +from core.logger import DialogType, Logger +from core.services.backup_service import BackupService +from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( + SimpleConfigParser, +) +from extensions.base_extension import BaseExtension +from extensions.tmc_autotune import ( + KLIPPER_DIR, + KLIPPER_EXTENSIONS_PATH, + TMCA_DIR, + TMCA_EXAMPLE_CONFIG, + TMCA_MOONRAKER_UPDATER_NAME, + TMCA_REPO, +) +from utils.config_utils import add_config_section, remove_config_section +from utils.fs_utils import check_file_exist, create_symlink, run_remove_routines +from utils.git_utils import git_clone_wrapper, git_pull_wrapper +from utils.input_utils import get_confirm +from utils.instance_utils import get_instances +from utils.sys_utils import check_python_version + + +# noinspection PyMethodMayBeStatic +class TmcAutotuneExtension(BaseExtension): + def install_extension(self, **kwargs) -> None: + Logger.print_status("Installing Klipper TMC Autotune...") + + # Check for Python 3.x, aligned with upstream install script + if not check_python_version(3, 0): + Logger.print_warn("Python 3.x is required. Aborting install.") + return + + klipper_dir_exists = check_file_exist(KLIPPER_DIR) + if not klipper_dir_exists: + Logger.print_warn( + "No Klipper directory found! Unable to install extension." + ) + return + + tmca_exists = ( + check_file_exist(TMCA_DIR) + and check_file_exist(KLIPPER_EXTENSIONS_PATH.joinpath("autotune_tmc.py")) + and check_file_exist(KLIPPER_EXTENSIONS_PATH.joinpath("motor_constants.py")) + and check_file_exist(KLIPPER_EXTENSIONS_PATH.joinpath("motor_database.cfg")) + ) + + overwrite = True + if tmca_exists: + overwrite = get_confirm( + question="Extension seems to be installed already. Overwrite?", + default_choice=True, + allow_go_back=False, + ) + + if not overwrite: + Logger.print_warn("Installation aborted due to user request.") + return + + add_moonraker_update_section = get_confirm( + question="Add Klipper TMC Autotune to Moonraker update manager(s)?", + default_choice=True, + allow_go_back=False, + ) + + create_example_config = get_confirm( + question="Create an example autotune_tmc.cfg for each instance?", + default_choice=True, + allow_go_back=False, + ) + + kl_instances = get_instances(Klipper) + + if not self._stop_klipper_instances_interactively( + kl_instances, "installation of TMC Autotune" + ): + return + + try: + git_clone_wrapper(TMCA_REPO, TMCA_DIR, force=True) + + Logger.print_info("Creating symlinks in Klipper extras directory...") + create_symlink( + TMCA_DIR.joinpath("autotune_tmc.py"), + KLIPPER_EXTENSIONS_PATH.joinpath("autotune_tmc.py"), + ) + create_symlink( + TMCA_DIR.joinpath("motor_constants.py"), + KLIPPER_EXTENSIONS_PATH.joinpath("motor_constants.py"), + ) + create_symlink( + TMCA_DIR.joinpath("motor_database.cfg"), + KLIPPER_EXTENSIONS_PATH.joinpath("motor_database.cfg"), + ) + Logger.print_ok( + "Symlinks created successfully for all instances.", end="\n\n" + ) + + if create_example_config: + self._install_example_cfg(kl_instances) + else: + Logger.print_info( + "Skipping example config creation as per user request." + ) + Logger.print_warn( + "Make sure to create and include an autotune_tmc.cfg in your printer.cfg in order to use the extension!" + ) + + if add_moonraker_update_section: + mr_instances = get_instances(Moonraker) + self._add_moonraker_update_manager_section(mr_instances) + else: + Logger.print_info( + "Skipping update section creation as per user request." + ) + Logger.print_warn( + "Make sure to create the corresponding section in your moonraker.conf in order to have it appear in your frontend update manager!" + ) + + except Exception as e: + Logger.print_error(f"Error during Klipper TMC Autotune installation:\n{e}") + + if kl_instances: + InstanceManager.start_all(kl_instances) + return + + if kl_instances: + InstanceManager.start_all(kl_instances) + + if create_example_config: + Logger.print_dialog( + DialogType.ATTENTION, + [ + "Basic configuration files were created per instance. You must edit them to enable the extension.", + "Documentation:", + f"{TMCA_REPO}", + "\n\n", + "IMPORTANT:", + "Define [autotune_tmc] sections ONLY in 'autotune_tmc.cfg'. ", + "Do NOT add them to 'printer.cfg', contrary to official docs. " + "While not fatal, mixing configs breaks file segmentation and is bad practice.", + ], + margin_bottom=1, + ) + + Logger.print_ok("Klipper TMC Autotune installed successfully!") + + def update_extension(self, **kwargs) -> None: + extension_installed = check_file_exist(TMCA_DIR) + if not extension_installed: + Logger.print_info("Extension does not seem to be installed! Skipping ...") + return + + backup_before_update = get_confirm( + question="Backup Klipper TMC Autotune directory before update?", + default_choice=True, + allow_go_back=True, + ) + + kl_instances = get_instances(Klipper) + + if not self._stop_klipper_instances_interactively( + kl_instances, "update of TMC Autotune" + ): + return + + Logger.print_status("Updating Klipper TMC Autotune...") + try: + if backup_before_update: + Logger.print_status("Backing up Klipper TMC Autotune directory...") + svc = BackupService() + svc.backup_directory( + source_path=TMCA_DIR, + backup_name="klipper_tmc_autotune", + ) + Logger.print_ok("Backup completed successfully.") + + git_pull_wrapper(TMCA_DIR) + + except Exception as e: + Logger.print_error(f"Error during Klipper TMC Autotune update:\n{e}") + + if kl_instances: + InstanceManager.start_all(kl_instances) + return + + if kl_instances: + InstanceManager.start_all(kl_instances) + + Logger.print_ok("Klipper TMC Autotune updated successfully.", end="\n\n") + + def remove_extension(self, **kwargs) -> None: + extension_installed = check_file_exist(TMCA_DIR) + if not extension_installed: + Logger.print_info("Extension does not seem to be installed! Skipping ...") + return + + kl_instances = get_instances(Klipper) + + if not self._stop_klipper_instances_interactively( + kl_instances, "removal of TMC Autotune" + ): + return + + try: + Logger.print_info("Removing Klipper TMC Autotune extension ...") + run_remove_routines(TMCA_DIR) + Logger.print_info("Removing symlinks from Klipper extras directory ...") + run_remove_routines(KLIPPER_EXTENSIONS_PATH.joinpath("autotune_tmc.py")) + run_remove_routines(KLIPPER_EXTENSIONS_PATH.joinpath("motor_constants.py")) + run_remove_routines(KLIPPER_EXTENSIONS_PATH.joinpath("motor_database.cfg")) + + mr_instances: List[Moonraker] = get_instances(Moonraker) + self._remove_moonraker_update_manager_section(mr_instances) + + Logger.print_info("Removing include from printer.cfg files ...") + BackupService().backup_printer_cfg() + remove_config_section("include autotune_tmc.cfg", kl_instances) + + Logger.print_dialog( + DialogType.ATTENTION, + [ + "Manual edits to 'printer.cfg' may be required if using exotic stepper configurations.", + "\n\n", + "NOTE:", + "'autotune_tmc.cfg' is NOT removed automatically. ", + "Please delete it manually if no longer needed.", + ], + margin_bottom=1, + ) + + except Exception as e: + Logger.print_error(f"Unable to remove extension:\n{e}") + + if kl_instances: + InstanceManager.start_all(kl_instances) + return + + if kl_instances: + InstanceManager.start_all(kl_instances) + + Logger.print_ok("Klipper TMC Autotune removed successfully.") + + def _install_example_cfg(self, kl_instances: List[Klipper]): + cfg_dirs = [instance.base.cfg_dir for instance in kl_instances] + + for cfg_dir in cfg_dirs: + Logger.print_status(f"Create autotune_tmc.cfg in '{cfg_dir}' ...") + if check_file_exist(cfg_dir.joinpath("autotune_tmc.cfg")): + Logger.print_info("File already exists! Skipping ...") + continue + try: + shutil.copy(TMCA_EXAMPLE_CONFIG, cfg_dir.joinpath("autotune_tmc.cfg")) + Logger.print_ok("Done!") + except OSError as e: + Logger.print_error(f"Unable to create example config: {e}") + + BackupService().backup_printer_cfg() + + section = "include autotune_tmc.cfg" + cfg_files = [instance.cfg_file for instance in kl_instances] + for cfg_file in cfg_files: + Logger.print_status(f"Include autotune_tmc.cfg in '{cfg_file}' ...") + scp = SimpleConfigParser() + scp.read_file(cfg_file) + if scp.has_section(section): + Logger.print_info("Section already defined! Skipping ...") + continue + scp.add_section(section) + scp.write_file(cfg_file) + Logger.print_ok("Done!") + + def _add_moonraker_update_manager_section( + self, mr_instances: List[Moonraker] + ) -> None: + if not mr_instances: + Logger.print_dialog( + DialogType.WARNING, + [ + "Moonraker not found! Klipper TMC Autotune update manager support " + "for Moonraker will not be added to moonraker.conf.", + ], + ) + if not get_confirm( + "Continue Klipper TMC Autotune installation?", + default_choice=False, + allow_go_back=True, + ): + Logger.print_info("Installation aborted due to user request.") + return + + BackupService().backup_moonraker_conf() + + add_config_section( + section=TMCA_MOONRAKER_UPDATER_NAME, + instances=mr_instances, + options=[ + ("type", "git_repo"), + ("channel", "dev"), + ("path", TMCA_DIR.as_posix()), + ("origin", TMCA_REPO), + ("managed_services", "klipper"), + ("primary_branch", "main"), + ], + ) + + InstanceManager.restart_all(mr_instances) + + Logger.print_ok( + "Klipper TMC Autotune successfully added to Moonraker update manager(s)!" + ) + + def _remove_moonraker_update_manager_section( + self, mr_instances: List[Moonraker] + ) -> None: + if not mr_instances: + Logger.print_dialog( + DialogType.WARNING, + [ + "Moonraker not found! Klipper TMC Autotune update manager support " + "for Moonraker will not be removed from moonraker.conf.", + ], + ) + return + + BackupService().backup_moonraker_conf() + + remove_config_section("update_manager klipper_tmc_autotune", mr_instances) + InstanceManager.restart_all(mr_instances) + + Logger.print_ok( + "Klipper TMC Autotune successfully removed from Moonraker update manager(s)!" + ) + + def _stop_klipper_instances_interactively( + self, kl_instances: List[Klipper], operation_name: str = "operation" + ) -> bool: + """ + Interactively stops all active Klipper instances, warning the user that ongoing prints will be disrupted. + + :param kl_instances: List of Klipper instances to stop. + :param operation_name: Optional name of the operation being performed (for user messaging). Do NOT capitalize. + :return: True if instances were stopped or no instances found, False if operation was aborted. + """ + + if not kl_instances: + Logger.print_warn("No instances found, skipping instance stopping.") + return True + + Logger.print_dialog( + DialogType.ATTENTION, + [ + "Do NOT continue if there are ongoing prints running", + f"All Klipper instances will be restarted during the {operation_name} and " + "ongoing prints WILL FAIL.", + ], + ) + stop_klipper = get_confirm( + question=f"Stop Klipper now and proceed with {operation_name}?", + default_choice=False, + allow_go_back=True, + ) + + if stop_klipper: + InstanceManager.stop_all(kl_instances) + return True + else: + Logger.print_warn( + f"{operation_name.capitalize()} aborted due to user request." + ) + return False