Compare commits

..

5 Commits

Author SHA1 Message Date
dw-0 2f41e52189 fix(core): refresh system package list in update menu after update (#791) 2026-04-19 12:21:17 +02:00
Théo Gaillard 757344128a fix(extensions_menu): prevent extension index collisions during loading (#783)
* fix(extensions_menu): prevent extension index collisions during loading

* feat(extensions_menu): add GITHUB_ISSUES_URL for reporting extension loading issues
2026-03-22 13:43:20 +01:00
Théo Gaillard 7ca08f9b30 feat(instance_manager): add interactive stopping of Klipper instances (#784)
* feat(instance_manager): add interactive stopping of Klipper instances with user confirmation

* fix(instance_utils): remove unnecessary 'self' parameter from stop_klipper_instances_interactively function

* refactor(tmc_autotune): replace internal stop function with direct call to stop_klipper_instances_interactively
2026-03-22 12:01:15 +01:00
David Rios 308079a821 fix: incorrect Logger.warn (#778)
Fix method name
2026-02-27 22:45:58 +01:00
Théo Gaillard 09a5d96b63 feat(tmc_autotune): add initial implementation of TMC Autotune extens… (#771)
* feat(tmc_autotune): add initial implementation of TMC Autotune extension with installation and update functionalities

* fix: remove useless comments

* fix: added support for Kalico style plugins directory

* refactor: extract method for moonraker update manager section removal, automatically reloading moonraker upon call
2026-02-03 20:46:24 +01:00
9 changed files with 473 additions and 7 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ def check_user_groups() -> None:
if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"):
log = "Skipped adding user to required groups. You might encounter issues."
Logger.warn(log)
Logger.print_warn(log)
return
try:
+9 -1
View File
@@ -251,6 +251,9 @@ class UpdateMenu(BaseMenu):
self._set_status_data("klipperscreen", get_klipperscreen_status)
self._set_status_data("crowsnest", get_crowsnest_status)
self._fetch_system_package_update_status()
def _fetch_system_package_update_status(self) -> None:
update_system_package_lists(silent=True)
self.packages = get_upgradable_packages()
self.package_count = len(self.packages)
@@ -340,15 +343,20 @@ class UpdateMenu(BaseMenu):
try:
pkgs: str = ", ".join(self.packages)
Logger.print_dialog(
DialogType.CUSTOM,
["The following packages will be upgraded:", "\n\n", pkgs],
custom_title="UPGRADABLE SYSTEM UPDATES",
)
if not get_confirm("Continue?"):
if not get_confirm("Upgrade packages?"):
return
Logger.print_status("Upgrading system packages ...")
upgrade_system_packages(self.packages)
self._fetch_system_package_update_status()
except Exception as e:
Logger.print_error(f"Error upgrading system packages:\n{e}")
raise
+1
View File
@@ -10,3 +10,4 @@
from pathlib import Path
EXTENSION_ROOT = Path(__file__).resolve().parents[1].joinpath("extensions")
GITHUB_ISSUES_URL = "https://github.com/dw-0/kiauh/issues"
+30 -4
View File
@@ -19,7 +19,7 @@ from core.logger import Logger
from core.menus import Option
from core.menus.base_menu import BaseMenu
from core.types.color import Color
from extensions import EXTENSION_ROOT
from extensions import EXTENSION_ROOT, GITHUB_ISSUES_URL
from extensions.base_extension import BaseExtension
@@ -56,6 +56,22 @@ class ExtensionsMenu(BaseMenu):
with open(metadata_json, "r") as m:
# read extension metadata from json
metadata = json.load(m).get("metadata")
index = str(metadata.get("index"))
# Prevent collisions where one extension silently overrides another.
if index in ext_dict:
existing_name = ext_dict[index].metadata.get("display_name")
duplicate_name = metadata.get("display_name")
Logger.print_warn(
"Failed loading extension"
f" {ext}: duplicate index '{index}'"
f" already used by '{existing_name}'."
f" Skipping '{duplicate_name}'."
f" Please report this at {GITHUB_ISSUES_URL}."
)
continue
int(index)
module_name = metadata.get("module")
module_path = f"kiauh.extensions.{ext.name}.{module_name}"
@@ -73,10 +89,20 @@ class ExtensionsMenu(BaseMenu):
# instantiate the extension with its metadata and add to dict
ext_instance: BaseExtension = ext_class(metadata)
ext_dict[f"{metadata.get('index')}"] = ext_instance
ext_dict[index] = ext_instance
except (IOError, json.JSONDecodeError, ImportError) as e:
print(f"Failed loading extension {ext}: {e}")
except (
IOError,
json.JSONDecodeError,
ImportError,
TypeError,
ValueError,
AttributeError,
) as e:
Logger.print_warn(
f"Failed loading extension {ext}: {e}. "
f"Please report this at {GITHUB_ISSUES_URL}."
)
return dict(sorted(ext_dict.items(), key=lambda x: int(x[0])))
+28
View File
@@ -0,0 +1,28 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# #
# 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"
@@ -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
}
}
@@ -0,0 +1,347 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# #
# 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, stop_klipper_instances_interactively
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 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 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 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)!"
)
+41
View File
@@ -12,8 +12,12 @@ import re
from pathlib import Path
from typing import List
from components.klipper.klipper import Klipper
from core.constants import SYSTEMD
from core.instance_manager.base_instance import SUFFIX_BLACKLIST
from core.instance_manager.instance_manager import InstanceManager
from core.logger import DialogType, Logger
from utils.input_utils import get_confirm
from utils.instance_type import InstanceType
@@ -56,3 +60,40 @@ def get_instance_suffix(name: str, file_path: Path) -> str:
# otherwise there is and hyphen left, and we return the part after the hyphen
suffix = file_path.stem[len(name) :]
return suffix[1:] if suffix else ""
def stop_klipper_instances_interactively(
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
+3 -1
View File
@@ -284,12 +284,14 @@ def get_upgradable_packages() -> List[str]:
try:
command = ["apt", "list", "--upgradable"]
output: str = check_output(command, stderr=DEVNULL, text=True, encoding="utf-8")
pkglist = []
pkglist: List[str] = []
for line in output.split("\n"):
if "/" not in line:
continue
pkg = line.split("/")[0]
pkglist.append(pkg)
return pkglist
except CalledProcessError as e:
raise Exception(f"Error reading upgradable packages: {e}")