Compare commits

...

8 Commits

Author SHA1 Message Date
dw-0 040edc1d4f chore: remove AGENTS.md 2026-06-28 12:50:59 +02:00
Paul 750dba1dbe feat(extensions): add Moongate for Klipper (#809)
* feat(extensions): add Moongate for Klipper

Adds Moongate for Klipper as extension #15 (next free index after KAMP). Moongate pairs a printer with the Moongate Android app for secure remote access and print monitoring over a Cloudflare quick-tunnel fronted by an EdDSA auth proxy.

The extension is KIAUH-native for the parts KIAUH owns (Moonraker instance discovery, the install/remove confirmation dialogs, moonraker.conf backup, and the repo clone wired into the update manager) and delegates the heavy, security-sensitive install steps (cloudflared, the moongate-authproxy and moongate-tunnel systemd services, the Moonraker host rebind) to Moongate's own idempotent, non-interactive install/update/uninstall scripts so that logic stays maintained upstream in one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(metadata): update index for Moongate extension

* refactor(moongate_extension): enhance error handling and update docstring for clarity

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2026-06-28 12:49:30 +02:00
Cody D Dixon 6f4b471008 feat(extension): Add DroidKlipp extension (#804)
* Add DroidKlipp extension

* fix(extensions): restart adb_monitor and redeploy monitor on DroidKlipp update

The update flow only ran git_pull_wrapper, which left the running
adb_monitor service executing stale in-memory code. It also never
rebuilt the deployed ~/droidklipp_monitor.py, since that file is
generated by the installer rather than tracked in the repo.

- stop adb_monitor before the pull, start it after (mirrors mobileraker)
- after pulling, re-deploy droidklipp_monitor.py into $HOME so the
  service picks up the updated code
- best-effort restart the service if the update fails

* fix(droidklipp): clean up comments and update website link in metadata

---------

Co-authored-by: Cody Dixon <codydixon71@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
2026-06-28 12:07:19 +02:00
GustavoTheThird 5077765fd6 fix: fluidd logo (#811)
* fix: fluid logo

* fix: use correct logo.svg instead of avatar

---------

Co-authored-by: dw-0 <th33xitus@gmail.com>
2026-06-27 12:53:20 +02:00
Patrick Gehrsitz 9f97ae6c2a refactor(crowsnest): fix crowsnest install status and update for v5 (#799)
fix: fix crowsnest install status and update for v5

Crowsnest v5 introduced multiple breaking changes resulting in a
'Incomplete' install status even after successful installation.
This adds support for the new v5 setup with backwards compatibility for
old versions.

Signed-off-by: Patrick Gehrsitz <github@mryel.de>
2026-05-30 15:36:32 +02:00
_Redstone_c_ b90a8f13b1 fix(switch_repo): honor use_python_binary when recreating venv (#793)
* fix(switch_repo): honor use_python_binary when recreating venv

* fix(switch_repo): validate name at function entry
2026-04-24 13:34:45 +02:00
Théo Gaillard ea1b794afd feat(extensions): Klipper-Adaptive-Meshing-Purging (#785)
* 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

* feat(klipper_adaptive_meshing_purging): add initial implementation of Klipper Adaptive Meshing and Purging extension

* docs(klipper_adaptive_meshing_purging): update warning message with additional documentation note regarding max_extrude_cross_section

* fix(klipper_adaptive_meshing_purging): update warning message formatting for clarity

* fix(klipper_adaptive_meshing_purging): update warning message formatting for clarity

* fix(_install_cfg): update symlink creation to point to Configuration directory

* fix(license): update copyright information to include myself & upstream

* fix(klipper_adaptive_meshing_purging): update moonraker updater name for consistency with upstream

* style: add type annotations

- Correct minor formatting issues in copyright headers.

* fix: update import path for `simple_config_parser`

* refactor: improve logging and configuration handling for KAMP installation and removal

* style: clarify warning message for purge settings in KAMP extension

---------

Co-authored-by: dw-0 <th33xitus@gmail.com>
2026-04-21 18:00:40 +02:00
dw-0 647a7d9400 feat: add initial AGENTS.md 2026-04-20 21:49:46 +02:00
14 changed files with 981 additions and 15 deletions
+1 -1
View File
@@ -147,7 +147,7 @@ changes!**
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
</tr>
<tr>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
<th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
</tr>
+2
View File
@@ -19,10 +19,12 @@ CROWSNEST_SERVICE_NAME = "crowsnest.service"
# directories
CROWSNEST_DIR = Path.home().joinpath("crowsnest")
CROWSNEST_ENV_DIR = Path.home().joinpath("crowsnest-env")
# files
CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config")
CROWSNEST_INSTALL_SCRIPT = CROWSNEST_DIR.joinpath("tools/install.sh")
CROWSNEST_DEPS_JSON_FILE = CROWSNEST_DIR.joinpath("system-dependencies.json")
CROWSNEST_BIN_FILE = Path("/usr/local/bin/crowsnest")
CROWSNEST_LOGROTATE_FILE = Path("/etc/logrotate.d/crowsnest")
CROWSNEST_SERVICE_FILE = SYSTEMD.joinpath(CROWSNEST_SERVICE_NAME)
+68 -8
View File
@@ -16,7 +16,9 @@ from typing import List
from components.crowsnest import (
CROWSNEST_BIN_FILE,
CROWSNEST_DEPS_JSON_FILE,
CROWSNEST_DIR,
CROWSNEST_ENV_DIR,
CROWSNEST_INSTALL_SCRIPT,
CROWSNEST_LOGROTATE_FILE,
CROWSNEST_MULTI_CONFIG,
@@ -25,6 +27,8 @@ from components.crowsnest import (
CROWSNEST_SERVICE_NAME,
)
from components.klipper.klipper import Klipper
from components.moonraker.utils.sysdeps_parser import SysDepsParser
from components.moonraker.utils.utils import load_sysdeps_json
from core.logger import DialogType, Logger
from core.services.backup_service import BackupService
from core.settings.kiauh_settings import KiauhSettings
@@ -34,6 +38,7 @@ from utils.common import (
get_install_status,
)
from utils.git_utils import (
get_current_branch,
git_clone_wrapper,
git_pull_wrapper,
)
@@ -135,8 +140,7 @@ def update_crowsnest() -> None:
git_pull_wrapper(CROWSNEST_DIR)
deps = parse_packages_from_file(CROWSNEST_INSTALL_SCRIPT)
check_install_dependencies({*deps})
install_crowsnest_packages()
cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "restart")
@@ -147,12 +151,68 @@ def update_crowsnest() -> None:
def get_crowsnest_status() -> ComponentStatus:
files = [
CROWSNEST_BIN_FILE,
CROWSNEST_LOGROTATE_FILE,
CROWSNEST_SERVICE_FILE,
]
return get_install_status(CROWSNEST_DIR, files=files)
"""
Get the current install status of Crowsnest. Depending on the version the installed
files are different. If a version is not yet specified, it will search for a
non_existant file resulting in 'Incomplete' status.
:return: Installation status
"""
files_dict = {
4: [
CROWSNEST_BIN_FILE,
CROWSNEST_LOGROTATE_FILE,
CROWSNEST_SERVICE_FILE,
],
5: [CROWSNEST_SERVICE_FILE],
}
version = get_crowsnest_version()
non_existant = CROWSNEST_DIR.joinpath("non_existant")
files = files_dict.get(version, [non_existant])
env_dir = None
if version >= 5:
env_dir = CROWSNEST_ENV_DIR
return get_install_status(CROWSNEST_DIR, files=files, env_dir=env_dir)
def get_crowsnest_version() -> int:
"""
Get the current major version. Starting with v5 the default branch will be named
after the major version.
:return: Current major version
"""
version = get_current_branch(CROWSNEST_DIR)
if version is None:
return 0
if version == "master":
return 4
return int(version.removeprefix("v"))
def install_crowsnest_packages() -> None:
Logger.print_status("Parsing Crowsnest system dependencies ...")
crowsnest_deps = []
crowsnest_version = get_crowsnest_version()
if crowsnest_version >= 5 and CROWSNEST_DEPS_JSON_FILE.exists():
Logger.print_info(
f"Parsing system dependencies from {CROWSNEST_DEPS_JSON_FILE.name} ..."
)
parser = SysDepsParser()
sysdeps = load_sysdeps_json(CROWSNEST_DEPS_JSON_FILE)
crowsnest_deps.extend(parser.parse_dependencies(sysdeps))
elif crowsnest_version <= 4 and CROWSNEST_INSTALL_SCRIPT.exists():
Logger.print_info(
f"Parsing system dependencies from {CROWSNEST_INSTALL_SCRIPT.name} ..."
)
crowsnest_deps = parse_packages_from_file(CROWSNEST_INSTALL_SCRIPT)
if not crowsnest_deps:
raise ValueError("Error parsing crowsnest dependencies!")
check_install_dependencies({*crowsnest_deps})
def remove_crowsnest() -> None:
+28
View File
@@ -0,0 +1,28 @@
# ======================================================================= #
# Copyright (C) 2026 Cody Dixon #
# #
# 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
DROIDKLIPP_REPO = "https://github.com/CodeMasterCody3D/DroidKlipp"
DROIDKLIPP_APK_URL = "https://github.com/CodeMasterCody3D/DroidKlipp-Android-APK/releases/latest/download/DroidKlipp.apk"
# directories
DROIDKLIPP_DIR = Path.home().joinpath("DroidKlipp")
# files
DROIDKLIPP_INSTALL_SCRIPT = DROIDKLIPP_DIR.joinpath("install_droidklipp.sh")
DROIDKLIPP_UNINSTALL_SCRIPT = DROIDKLIPP_DIR.joinpath("uninstall_droidklipp.sh")
DROIDKLIPP_MONITOR_FILE = DROIDKLIPP_DIR.joinpath("droidklipp_monitor.py")
DROIDKLIPP_DEPLOYED_MONITOR = Path.home().joinpath("droidklipp_monitor.py")
# service
DROIDKLIPP_SERVICE_NAME = "adb_monitor"
# packages
DROIDKLIPP_REQUIRED_PACKAGES = {"adb", "tmux", "x11-utils"}
@@ -0,0 +1,155 @@
# ======================================================================= #
# Copyright (C) 2026 Cody Dixon #
# #
# 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 subprocess import CalledProcessError, run
from components.klipperscreen import KLIPPERSCREEN_DIR, KLIPPERSCREEN_ENV_DIR
from core.logger import DialogType, Logger
from extensions.base_extension import BaseExtension
from extensions.droidklipp import (
DROIDKLIPP_APK_URL,
DROIDKLIPP_DEPLOYED_MONITOR,
DROIDKLIPP_DIR,
DROIDKLIPP_INSTALL_SCRIPT,
DROIDKLIPP_MONITOR_FILE,
DROIDKLIPP_REPO,
DROIDKLIPP_REQUIRED_PACKAGES,
DROIDKLIPP_SERVICE_NAME,
DROIDKLIPP_UNINSTALL_SCRIPT,
)
from utils.common import check_install_dependencies
from utils.fs_utils import check_file_exist, run_remove_routines
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm
from utils.sys_utils import cmd_sysctl_service
# noinspection PyMethodMayBeStatic
class DroidKlippExtension(BaseExtension):
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing DroidKlipp ...")
if not self._klipperscreen_exists():
Logger.print_dialog(
DialogType.WARNING,
[
"No KIAUH v6 KlipperScreen installation found!",
"DroidKlipp expects KlipperScreen at:",
f"{KLIPPERSCREEN_DIR.joinpath('screen.py')}",
f"{KLIPPERSCREEN_ENV_DIR.joinpath('bin/python')}",
"Install KlipperScreen first, then run this installer again.",
],
)
return
Logger.print_dialog(
DialogType.INFO,
[
"DroidKlipp requires the Android APK to be installed on your Android device:",
DROIDKLIPP_APK_URL,
"\n\n",
"The installer will configure ADB forwarding, udev rules, the DroidKlipp monitor, and WiFi fallback.",
],
)
if not get_confirm(
"Continue DroidKlipp installation?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting DroidKlipp installation ...")
return
try:
check_install_dependencies(DROIDKLIPP_REQUIRED_PACKAGES)
git_clone_wrapper(DROIDKLIPP_REPO, DROIDKLIPP_DIR)
run(["chmod", "+x", DROIDKLIPP_INSTALL_SCRIPT], check=True)
run([DROIDKLIPP_INSTALL_SCRIPT], check=True)
Logger.print_dialog(
DialogType.SUCCESS,
["DroidKlipp successfully installed!"],
center_content=True,
)
except CalledProcessError as e:
Logger.print_error(f"Error during DroidKlipp installation:\n{e}")
except Exception as e:
Logger.print_error(f"Error during DroidKlipp installation:\n{e}")
def update_extension(self, **kwargs) -> None:
Logger.print_status("Updating DroidKlipp ...")
if not check_file_exist(DROIDKLIPP_DIR):
Logger.print_info("Extension does not seem to be installed! Skipping ...")
return
try:
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "stop")
git_pull_wrapper(DROIDKLIPP_DIR)
if check_file_exist(DROIDKLIPP_MONITOR_FILE):
run(
[
"install",
"-m",
"755",
str(DROIDKLIPP_MONITOR_FILE),
str(DROIDKLIPP_DEPLOYED_MONITOR),
],
check=True,
)
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "start")
Logger.print_dialog(
DialogType.SUCCESS,
["DroidKlipp successfully updated!"],
center_content=True,
)
except CalledProcessError as e:
Logger.print_error(f"Error during DroidKlipp update:\n{e}")
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "start")
except Exception as e:
Logger.print_error(f"Error during DroidKlipp update:\n{e}")
cmd_sysctl_service(DROIDKLIPP_SERVICE_NAME, "start")
def remove_extension(self, **kwargs) -> None:
Logger.print_status("Removing DroidKlipp ...")
if not check_file_exist(DROIDKLIPP_DIR):
Logger.print_info("Extension does not seem to be installed! Skipping ...")
return
if not get_confirm(
"Do you really want to uninstall DroidKlipp?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Exiting DroidKlipp uninstallation ...")
return
try:
if check_file_exist(DROIDKLIPP_UNINSTALL_SCRIPT):
run(["chmod", "+x", DROIDKLIPP_UNINSTALL_SCRIPT], check=True)
run([DROIDKLIPP_UNINSTALL_SCRIPT], check=True)
run_remove_routines(DROIDKLIPP_DIR)
Logger.print_dialog(
DialogType.SUCCESS,
["DroidKlipp successfully removed!"],
center_content=True,
)
except CalledProcessError as e:
Logger.print_error(f"Error during DroidKlipp removal:\n{e}")
except Exception as e:
Logger.print_error(f"Error during DroidKlipp removal:\n{e}")
def _klipperscreen_exists(self) -> bool:
return bool(
check_file_exist(KLIPPERSCREEN_DIR.joinpath("screen.py"))
and check_file_exist(KLIPPERSCREEN_ENV_DIR.joinpath("bin/python"))
)
+17
View File
@@ -0,0 +1,17 @@
{
"metadata": {
"index": 15,
"module": "droidklipp_extension",
"maintained_by": "CodeMasterCody3D",
"display_name": "DroidKlipp",
"description": [
"Use an Android device as a KlipperScreen display via ADB and DroidKlipp APK / XServer XSDL integration",
"- Automatic USB ADB forwarding",
"- Optional WiFi fallback",
"- Starts and monitors KlipperScreen on the Android X server"
],
"website": "https://github.com/CodeMasterCody3D/DroidKlipp-Android-APK/releases",
"repo": "https://github.com/CodeMasterCody3D/DroidKlipp",
"updates": true
}
}
@@ -0,0 +1,21 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2026 Théo Gaillard <theo.gayar@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
KAMP_REPO = "https://github.com/kyleisah/Klipper-Adaptive-Meshing-Purging"
# directories
MODULE_PATH = Path(__file__).resolve().parent
KAMP_DIR = Path.home().joinpath("Klipper-Adaptive-Meshing-Purging")
KLIPPER_DIR = Path.home().joinpath("klipper")
# names
KAMP_MOONRAKER_UPDATER_NAME = "update_manager Klipper-Adaptive-Meshing-Purging"
@@ -0,0 +1,357 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2026 Théo Gaillard <theo.gayar@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# It integrates Klipper Adaptive Meshing & Purging (KAMP): #
# https://github.com/kyleisah/Klipper-Adaptive-Meshing-Purging #
# #
# 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.simple_config_parser.simple_config_parser import (
SimpleConfigParser,
)
from extensions.base_extension import BaseExtension
from extensions.klipper_adaptive_meshing_purging import (
KAMP_DIR,
KAMP_MOONRAKER_UPDATER_NAME,
KAMP_REPO,
KLIPPER_DIR,
)
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
# noinspection PyMethodMayBeStatic
class KlipperAdaptiveMeshingPurgingExtension(BaseExtension):
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing Klipper Adaptive Meshing Purging...")
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
kl_instances: List[Klipper] = get_instances(Klipper)
kamp_exists = check_file_exist(KAMP_DIR) and self._check_cfg_exists(
kl_instances
)
overwrite = True
if kamp_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 Adaptive Meshing and Purging to Moonraker update manager(s)?",
default_choice=True,
allow_go_back=False,
)
if not stop_klipper_instances_interactively(
kl_instances, "installation of KAMP"
):
return
try:
git_clone_wrapper(KAMP_REPO, KAMP_DIR, force=True)
self._install_cfg(kl_instances)
if add_moonraker_update_section:
mr_instances: List[Moonraker] = 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 Adaptive Meshing and Purging installation:\n{e}"
)
if kl_instances:
InstanceManager.start_all(kl_instances)
return
if kl_instances:
InstanceManager.start_all(kl_instances)
Logger.print_dialog(
DialogType.ATTENTION,
[
"Basic configuration files were created per instance. You must edit them to enable the extension.",
"Documentation:",
f"{KAMP_REPO}",
"\n\n",
"IMPORTANT:",
"1. If you'd like to use adaptive meshing, Klipper already has built-in support. Just call BED_MESH_CALIBRATE ADAPTIVE=1 in your PRINT_START macro. DO NOT USE THE FEATURE FROM THE EXTENSION",
"2. You MUST be thoughtful when editing values for the purge settings, as there is a risk to break parts of your printer.",
"3. According to KAMP's documentation, you should define 'max_extrude_cross_section' in 'printer.cfg' according to your needs.",
],
margin_bottom=1,
)
Logger.print_ok("Klipper Adaptive Meshing and Purging installed successfully!")
def update_extension(self, **kwargs) -> None:
extension_installed = check_file_exist(KAMP_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 Adaptive Meshing and Purging directory before update?",
default_choice=True,
allow_go_back=True,
)
kl_instances: List[Klipper] = get_instances(Klipper)
if not stop_klipper_instances_interactively(kl_instances, "update of KAMP"):
return
Logger.print_status("Updating Klipper Adaptive Meshing and Purging...")
try:
if backup_before_update:
Logger.print_status(
"Backing up Klipper Adaptive Meshing and Purging directory ..."
)
svc = BackupService()
svc.backup_directory(
source_path=KAMP_DIR,
backup_name="Klipper-Adaptive-Meshing-Purging",
)
Logger.print_ok("Backup completed successfully.")
git_pull_wrapper(KAMP_DIR)
except Exception as e:
Logger.print_error(
f"Error during Klipper Adaptive Meshing and Purging update:\n{e}"
)
if kl_instances:
InstanceManager.start_all(kl_instances)
return
if kl_instances:
InstanceManager.start_all(kl_instances)
Logger.print_ok(
"Klipper Adaptive Meshing and Purging updated successfully.", end="\n\n"
)
def remove_extension(self, **kwargs) -> None:
extension_installed = check_file_exist(KAMP_DIR)
if not extension_installed:
Logger.print_info("Extension does not seem to be installed! Skipping ...")
return
kl_instances: List[Klipper] = get_instances(Klipper)
if not stop_klipper_instances_interactively(kl_instances, "removal of KAMP"):
return
try:
run_remove_routines(KAMP_DIR)
self._remove_cfg(kl_instances)
mr_instances: List[Moonraker] = get_instances(Moonraker)
self._remove_moonraker_update_manager_section(mr_instances)
BackupService().backup_printer_cfg()
remove_config_section("include KAMP_Settings.cfg", kl_instances)
Logger.print_dialog(
DialogType.ATTENTION,
[
"You might want to remove [exclude_object] sections from 'printer.cfg', unless you use them for some other reason.",
"\n\n",
"You might also want to remove the [file_manager] sections from 'moonraker.conf', unless used otherwise.",
"\n\n",
"NOTE:",
"'KAMP_Settings.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 Adaptive Meshing and Purging removed successfully.")
def _install_cfg(self, kl_instances: List[Klipper]):
is_multi_instance = len(kl_instances) > 1
for instance in kl_instances:
cfg_dir = instance.base.cfg_dir
Logger.print_status(
f"Creating symlink for KAMP directory in '{cfg_dir}' ..."
)
create_symlink(KAMP_DIR.joinpath("Configuration"), cfg_dir.joinpath("KAMP"))
if is_multi_instance:
Logger.print_ok(f"Symlink successfully created for instance '{instance.suffix}'.")
else:
Logger.print_ok("Symlink successfully created.")
# We do not overwrite the existing config files ever
Logger.print_status(f"Creating KAMP_Settings.cfg in '{cfg_dir}' ...")
if check_file_exist(cfg_dir.joinpath("KAMP_Settings.cfg")):
Logger.print_info("File already exists! Skipping ...")
continue
try:
shutil.copy(
KAMP_DIR.joinpath("Configuration/KAMP_Settings.cfg"),
cfg_dir.joinpath("KAMP_Settings.cfg"),
)
if is_multi_instance:
Logger.print_ok(f"Config file successfully created for instance '{instance.suffix}'.")
else:
Logger.print_ok(f"Config file successfully created.")
except OSError as e:
Logger.print_error(f"Unable to create example config: {e}")
BackupService().backup_printer_cfg()
sections = ["include KAMP_Settings.cfg", "exclude_object"]
for instance in kl_instances:
cfg_file = instance.cfg_file
scp = SimpleConfigParser()
scp.read_file(cfg_file)
for section in sections:
if scp.has_section(section):
continue
Logger.print_status(f"Add '{section}' to '{cfg_file}' ...")
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 Adaptive Meshing and Purging update "
"manager support for Moonraker will not be added to moonraker.conf.",
],
)
if not get_confirm(
"Continue Klipper Adaptive Meshing and Purging 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=KAMP_MOONRAKER_UPDATER_NAME,
instances=mr_instances,
options=[
("type", "git_repo"),
("channel", "dev"),
("path", KAMP_DIR.as_posix()),
("origin", KAMP_REPO),
("managed_services", "klipper"),
("primary_branch", "main"),
],
)
add_config_section(
section="file_manager",
instances=mr_instances,
options=[
("enable_object_processing", "True"),
],
)
InstanceManager.restart_all(mr_instances)
Logger.print_ok(
"Klipper Adaptive Meshing and Purging 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 Adaptive Meshing and Purging update "
"manager support for Moonraker will not be removed from moonraker.conf.",
],
)
return
BackupService().backup_moonraker_conf()
remove_config_section(KAMP_MOONRAKER_UPDATER_NAME, mr_instances)
InstanceManager.restart_all(mr_instances)
Logger.print_ok(
"Klipper Adaptive Meshing and Purging successfully removed from Moonraker update manager(s)!"
)
def _check_cfg_exists(self, kl_instances: List[Klipper]) -> bool:
cfg_dirs = [instance.base.cfg_dir for instance in kl_instances]
for cfg_dir in cfg_dirs:
if (
check_file_exist(cfg_dir.joinpath("KAMP_Settings.cfg"))
and check_file_exist(cfg_dir.joinpath("KAMP/KAMP_Settings.cfg"))
and check_file_exist(cfg_dir.joinpath("KAMP/Adaptive_Meshing.cfg"))
and check_file_exist(cfg_dir.joinpath("KAMP/Line_Purge.cfg"))
and check_file_exist(cfg_dir.joinpath("KAMP/Smart_Park.cfg"))
and check_file_exist(cfg_dir.joinpath("KAMP/Voron_Purge.cfg"))
):
return True
return False
def _remove_cfg(self, kl_instances: List[Klipper]) -> None:
cfg_dirs = [instance.base.cfg_dir for instance in kl_instances]
for cfg_dir in cfg_dirs:
Logger.print_status(f"Removing KAMP symlink in '{cfg_dir}' ...")
run_remove_routines(cfg_dir.joinpath("KAMP"))
@@ -0,0 +1,13 @@
{
"metadata": {
"index": 14,
"module": "klipper_adaptive_meshing_purging_extension",
"maintained_by": "theogayar",
"display_name": "Klipper Adaptive Meshing Purging",
"description": [
"Klipper extension for adaptive meshing and purging."
],
"repo": "https://github.com/kyleisah/Klipper-Adaptive-Meshing-Purging",
"updates": true
}
}
+34
View File
@@ -0,0 +1,34 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2026 Paul Sharman <github.com/PEEKYPAUL> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# It integrates Moongate for Klipper: #
# https://github.com/PEEKYPAUL/Moongate #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from pathlib import Path
# repository
MOONGATE_REPO = "https://github.com/PEEKYPAUL/moongate.git"
MOONGATE_REPO_URL = "https://github.com/PEEKYPAUL/Moongate"
# directories
MODULE_PATH = Path(__file__).resolve().parent
MOONGATE_DIR = Path.home().joinpath("moongate")
MOONGATE_PLUGIN_DIR = MOONGATE_DIR.joinpath("klipper-plugin")
# installer scripts shipped inside the cloned repo
MOONGATE_INSTALL_SCRIPT = MOONGATE_PLUGIN_DIR.joinpath("install.sh")
MOONGATE_UPDATE_SCRIPT = MOONGATE_PLUGIN_DIR.joinpath("update.sh")
MOONGATE_UNINSTALL_SCRIPT = MOONGATE_PLUGIN_DIR.joinpath("uninstall.sh")
# moonraker.conf sections the installer manages
MOONGATE_UPDATER_NAME = "update_manager moongate"
MOONGATE_CONFIG_SECTION = "moongate"
# default HTTP port the Mainsail/Fluidd UI is served on
MOONGATE_DEFAULT_PORT = 80
+15
View File
@@ -0,0 +1,15 @@
{
"metadata": {
"index": 16,
"module": "moongate_extension",
"maintained_by": "PEEKYPAUL",
"display_name": "Moongate for Klipper",
"description": [
"Pair this printer with the Moongate Android app for secure remote",
"access and print monitoring. Installs cloudflared, a Cloudflare",
"quick-tunnel and an EdDSA auth gate in front of Moonraker."
],
"repo": "https://github.com/PEEKYPAUL/Moongate",
"updates": true
}
}
@@ -0,0 +1,249 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2026 Paul Sharman <github.com/PEEKYPAUL> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
# #
# It integrates Moongate for Klipper: #
# https://github.com/PEEKYPAUL/Moongate #
# #
# This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= #
from __future__ import annotations
import os
from pathlib import Path
from subprocess import CalledProcessError, run
from typing import Dict, List
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 extensions.base_extension import BaseExtension
from extensions.moongate import (
MOONGATE_CONFIG_SECTION,
MOONGATE_DEFAULT_PORT,
MOONGATE_DIR,
MOONGATE_INSTALL_SCRIPT,
MOONGATE_REPO,
MOONGATE_REPO_URL,
MOONGATE_UNINSTALL_SCRIPT,
MOONGATE_UPDATE_SCRIPT,
MOONGATE_UPDATER_NAME,
)
from utils.config_utils import remove_config_section
from utils.fs_utils import check_file_exist
from utils.git_utils import GitException, git_clone_wrapper, git_pull_wrapper
from utils.input_utils import get_confirm, get_number_input
from utils.instance_utils import get_instances
# noinspection PyMethodMayBeStatic
class MoongateExtension(BaseExtension):
"""
Moongate ships a substantial, security-sensitive and idempotent installer
(cloudflared, two systemd services, an EdDSA auth proxy, a Moonraker host
rebind and a tightly-scoped Avahi sudoers entry). Rather than mirror all
of that in Python — where it would drift out of sync with upstream — this
extension does the KIAUH-idiomatic parts natively (instance discovery,
confirmation, moonraker.conf backup, the repo clone wired to the update
manager) and delegates the heavy lifting to Moongate's own scripts.
"""
def install_extension(self, **kwargs) -> None:
Logger.print_status("Installing Moongate for Klipper ...")
mr_instances: List[Moonraker] = get_instances(Moonraker)
if not mr_instances:
Logger.print_dialog(
DialogType.WARNING,
[
"No Moonraker instances found!",
"Moongate is a Moonraker component and needs Moonraker to be "
"installed first. Please install Moonraker, then try again.",
],
)
return
# Moongate is a single-printer integration. On a multi-instance host we
# target the first Moonraker instance and say so.
moonraker = mr_instances[0]
if len(mr_instances) > 1:
Logger.print_dialog(
DialogType.WARNING,
[
"Multiple Moonraker instances detected.",
"Moongate currently supports a single-printer setup. The "
f"instance '{moonraker.data_dir.name}' will be used.",
],
)
if not self._confirm_install():
Logger.print_info("Installation aborted.")
return
port = get_number_input(
"HTTP port your Mainsail/Fluidd UI is served on",
min_value=1,
max_value=65535,
default=MOONGATE_DEFAULT_PORT,
)
if port is None:
return
try:
self._clone_or_update_repo()
BackupService().backup_moonraker_conf()
# Hand off to Moongate's own installer. It is idempotent,
# non-interactive and env-driven: it installs cloudflared, adds the
# two systemd services, patches moonraker.conf and restarts
# Moonraker + Klipper itself.
self._run_script(
MOONGATE_INSTALL_SCRIPT,
moonraker,
extra_env={"MOONGATE_PORT": str(port)},
)
except (GitException, CalledProcessError, OSError) as e:
Logger.print_error(f"Error during Moongate installation:\n{e}")
return
Logger.print_dialog(
DialogType.SUCCESS,
[
"Moongate installed successfully!",
"\n\n",
"Next steps:",
"● Install the Moongate app on your Android device.",
"● Run MOONGATE_PAIR in the Klipper console (or open the pair "
"page printed above) and scan the QR code.",
"● Updates from now on: Mainsail/Fluidd > Software Updates > Moongate.",
],
margin_bottom=1,
)
def update_extension(self, **kwargs) -> None:
Logger.print_status("Updating Moongate for Klipper ...")
if not check_file_exist(MOONGATE_DIR.joinpath(".git")):
Logger.print_info("Moongate does not seem to be installed. Skipping ...")
return
mr_instances: List[Moonraker] = get_instances(Moonraker)
if not mr_instances:
Logger.print_warn("No Moonraker instance found. Skipping ...")
return
try:
git_pull_wrapper(MOONGATE_DIR)
self._run_script(MOONGATE_UPDATE_SCRIPT, mr_instances[0])
InstanceManager.restart_all(mr_instances)
except (GitException, CalledProcessError, OSError) as e:
Logger.print_error(f"Error during Moongate update:\n{e}")
return
Logger.print_ok("Moongate updated successfully.", end="\n\n")
def remove_extension(self, **kwargs) -> None:
Logger.print_status("Removing Moongate for Klipper ...")
mr_instances: List[Moonraker] = get_instances(Moonraker)
if not get_confirm(
"This removes Moongate, cloudflared, both systemd services and all "
"Moongate config. Continue?",
default_choice=True,
allow_go_back=True,
):
Logger.print_info("Removal aborted.")
return
# Preferred path: delegate to Moongate's own uninstaller, which stops
# and removes the services, cleans moonraker.conf, restores its backup
# and restarts Moonraker. MOONGATE_YES=1 makes it non-interactive
# (KIAUH already collected the confirmation above).
if check_file_exist(MOONGATE_UNINSTALL_SCRIPT):
try:
BackupService().backup_moonraker_conf()
target = mr_instances[0] if mr_instances else None
self._run_script(
MOONGATE_UNINSTALL_SCRIPT,
target,
extra_env={"MOONGATE_YES": "1"},
)
Logger.print_ok("Moongate removed successfully.")
return
except (CalledProcessError, OSError) as e:
Logger.print_error(f"Error during Moongate removal:\n{e}")
# fall through to a best-effort native cleanup
# Fallback: the upstream uninstaller is gone (repo already deleted).
# Do a best-effort native cleanup so moonraker.conf is left consistent.
Logger.print_warn(
"Moongate uninstaller not found — doing a best-effort cleanup. You "
"may need to remove cloudflared and the moongate-* systemd services "
"manually."
)
if mr_instances:
BackupService().backup_moonraker_conf()
remove_config_section(MOONGATE_UPDATER_NAME, mr_instances)
remove_config_section(MOONGATE_CONFIG_SECTION, mr_instances)
InstanceManager.restart_all(mr_instances)
Logger.print_ok("Moongate configuration removed.")
# ------------------------------------------------------------------ #
# helpers #
# ------------------------------------------------------------------ #
def _confirm_install(self) -> bool:
Logger.print_dialog(
DialogType.ATTENTION,
[
"Moongate pairs this printer with the Moongate Android app for "
"secure remote access and print monitoring.",
"\n\n",
"This is a heavier install than most extensions. It will:",
"● clone the Moongate repo to ~/moongate",
"● add the Moongate component to Moonraker and register it with "
"the update manager",
"● install cloudflared and open a Cloudflare quick-tunnel",
"● add two systemd services: moongate-authproxy + moongate-tunnel",
"● bind Moonraker to 127.0.0.1 (the auth proxy fronts the tunnel)",
"● add a tightly-scoped Avahi sudoers entry for LAN discovery",
"\n\n",
"Remote access relies on cloud infrastructure operated by the "
"Moongate author. Moongate is licensed under PolyForm "
"Noncommercial 1.0.0 (non-commercial use only).",
MOONGATE_REPO_URL,
],
margin_bottom=1,
)
return bool(
get_confirm(
"Continue Moongate installation?",
default_choice=True,
allow_go_back=True,
)
)
def _clone_or_update_repo(self) -> None:
if check_file_exist(MOONGATE_DIR.joinpath(".git")):
git_pull_wrapper(MOONGATE_DIR)
else:
git_clone_wrapper(MOONGATE_REPO, MOONGATE_DIR)
def _run_script(
self,
script: Path,
moonraker: Moonraker | None,
extra_env: Dict[str, str] | None = None,
) -> None:
env = os.environ.copy()
if moonraker is not None:
env["MOONRAKER_DIR"] = moonraker.moonraker_dir.as_posix()
env["PRINTER_DATA"] = moonraker.data_dir.as_posix()
if extra_env:
env.update(extra_env)
run(["bash", script.as_posix()], env=env, check=True)
@@ -1,5 +1,6 @@
# ======================================================================= #
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
# Copyright (C) 2026 Théo Gaillard <theo.gayar@gmail.com> #
# #
# This file is part of KIAUH - Klipper Installation And Update Helper #
# https://github.com/dw-0/kiauh #
@@ -82,7 +83,7 @@ class TmcAutotuneExtension(BaseExtension):
allow_go_back=False,
)
kl_instances = get_instances(Klipper)
kl_instances: List[Klipper] = get_instances(Klipper)
if not stop_klipper_instances_interactively(
kl_instances, "installation of TMC Autotune"
@@ -120,7 +121,7 @@ class TmcAutotuneExtension(BaseExtension):
)
if add_moonraker_update_section:
mr_instances = get_instances(Moonraker)
mr_instances: List[Moonraker] = get_instances(Moonraker)
self._add_moonraker_update_manager_section(mr_instances)
else:
Logger.print_info(
@@ -170,7 +171,7 @@ class TmcAutotuneExtension(BaseExtension):
allow_go_back=True,
)
kl_instances = get_instances(Klipper)
kl_instances: List[Klipper] = get_instances(Klipper)
if not stop_klipper_instances_interactively(
kl_instances, "update of TMC Autotune"
@@ -208,7 +209,7 @@ class TmcAutotuneExtension(BaseExtension):
Logger.print_info("Extension does not seem to be installed! Skipping ...")
return
kl_instances = get_instances(Klipper)
kl_instances: List[Klipper] = get_instances(Klipper)
if not stop_klipper_instances_interactively(
kl_instances, "removal of TMC Autotune"
@@ -344,4 +345,3 @@ class TmcAutotuneExtension(BaseExtension):
Logger.print_ok(
"Klipper TMC Autotune successfully removed from Moonraker update manager(s)!"
)
+16 -1
View File
@@ -31,6 +31,7 @@ from components.moonraker.services.moonraker_setup_service import (
from core.instance_manager.instance_manager import InstanceManager
from core.logger import Logger
from core.services.backup_service import BackupService
from core.settings.kiauh_settings import KiauhSettings
from utils.git_utils import GitException, git_clone_wrapper
from utils.instance_utils import get_instances
from utils.sys_utils import (
@@ -47,6 +48,11 @@ class RepoSwitchFailedException(Exception):
def run_switch_repo_routine(
name: Literal["klipper", "moonraker"], repo_url: str, branch: str
) -> None:
if name not in ("klipper", "moonraker"):
raise ValueError(
f"Invalid name: {name!r}. Must be 'klipper' or 'moonraker'."
)
repo_dir: Path = KLIPPER_DIR if name == "klipper" else MOONRAKER_DIR
env_dir: Path = KLIPPER_ENV_DIR if name == "klipper" else MOONRAKER_ENV_DIR
req_file = KLIPPER_REQ_FILE if name == "klipper" else MOONRAKER_REQ_FILE
@@ -89,7 +95,16 @@ def run_switch_repo_routine(
# step 6: recreate python virtualenv
Logger.print_status(f"Recreating {_type.__name__} virtualenv ...")
if not create_python_venv(env_dir, force=True):
settings = KiauhSettings()
if name == "klipper":
use_python_binary = settings.klipper.use_python_binary
elif name == "moonraker":
use_python_binary = settings.moonraker.use_python_binary
if not create_python_venv(
env_dir, force=True, use_python_binary=use_python_binary
):
raise GitException(f"Failed to recreate virtualenv for {_type.__name__}")
else:
install_python_requirements(env_dir, req_file)