diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py index 908ed4e..fd63b5c 100644 --- a/kiauh/components/klipper/__init__.py +++ b/kiauh/components/klipper/__init__.py @@ -25,6 +25,7 @@ KLIPPER_SERVICE_NAME = "klipper.service" # directories KLIPPER_DIR = Path.home().joinpath("klipper") +KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs") KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py index 435a96d..98c8492 100644 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ b/kiauh/components/klipper_firmware/firmware_utils.py @@ -138,6 +138,7 @@ def start_flash_process(flash_options: FlashOptions) -> None: if flash_options.flash_method is FlashMethod.REGULAR: cmd = [ "make", + f"KCONFIG_CONFIG={flash_options.selected_kconfig}", flash_options.flash_command.value, f"FLASH_DEVICE={flash_options.selected_mcu}", ] @@ -172,10 +173,10 @@ def start_flash_process(flash_options: FlashOptions) -> None: Logger.print_error("See the console output above!", end="\n\n") -def run_make_clean() -> None: +def run_make_clean(kconfig = '.config') -> None: try: run( - "make clean", + f"make KCONFIG_CONFIG={kconfig} clean", cwd=KLIPPER_DIR, shell=True, check=True, @@ -185,10 +186,10 @@ def run_make_clean() -> None: raise -def run_make_menuconfig() -> None: +def run_make_menuconfig(kconfig = '.config') -> None: try: run( - "make PYTHON=python3 menuconfig", + f"make PYTHON=python3 KCONFIG_CONFIG={kconfig} menuconfig", cwd=KLIPPER_DIR, shell=True, check=True, @@ -198,10 +199,10 @@ def run_make_menuconfig() -> None: raise -def run_make() -> None: +def run_make(kconfig = '.config') -> None: try: run( - "make PYTHON=python3", + f"make PYTHON=python3 KCONFIG_CONFIG={kconfig}", cwd=KLIPPER_DIR, shell=True, check=True, diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py index 023df67..22e3fd9 100644 --- a/kiauh/components/klipper_firmware/flash_options.py +++ b/kiauh/components/klipper_firmware/flash_options.py @@ -39,6 +39,7 @@ class FlashOptions: _selected_mcu: str = "" _selected_board: str = "" _selected_baudrate: int = 250000 + _selected_kconfig: str = ".config" def __new__(cls, *args, **kwargs): if not cls._instance: @@ -104,3 +105,11 @@ class FlashOptions: @selected_baudrate.setter def selected_baudrate(self, value: int) -> None: self._selected_baudrate = value + + @property + def selected_kconfig(self) -> str: + return self._selected_kconfig + + @selected_kconfig.setter + def selected_kconfig(self, value: str) -> None: + self._selected_kconfig = value diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py index 1237eed..0957811 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -10,34 +10,121 @@ from __future__ import annotations import textwrap from typing import List, Set, Type +from os import path, listdir, mkdir +from shutil import copyfile -from components.klipper import KLIPPER_DIR +from components.klipper import KLIPPER_DIR, KLIPPER_KCONFIGS_DIR from components.klipper_firmware.firmware_utils import ( run_make, run_make_clean, run_make_menuconfig, ) -from core.logger import Logger +from components.klipper_firmware.flash_options import FlashOptions +from core.logger import DialogType, Logger from core.menus import Option -from core.menus.base_menu import BaseMenu +from core.menus.base_menu import BaseMenu, print_back_footer from core.types.color import Color from utils.sys_utils import ( check_package_install, install_system_packages, update_system_package_lists, ) +from utils.input_utils import get_confirm, get_string_input, get_selection_input # noinspection PyUnusedLocal # noinspection PyMethodMayBeStatic -class KlipperBuildFirmwareMenu(BaseMenu): +class KlipperKConfigMenu(BaseMenu): def __init__(self, previous_menu: Type[BaseMenu] | None = None): + super().__init__() + self.title = "Firmware Config Menu" + self.title_color = Color.CYAN + self.previous_menu: Type[BaseMenu] | None = previous_menu + self.flash_options = FlashOptions() + self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR + self.kconfig_default = path.join(KLIPPER_DIR, ".config") + self.kconfig = self.kconfig_default if not path.isdir(self.kconfigs_dirname) else None + + def run(self) -> None: + if not self.kconfig: + super().run() + else: + self.flash_options.selected_kconfig = self.kconfig + + def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: + from core.menus.advanced_menu import AdvancedMenu + + self.previous_menu = ( + previous_menu if previous_menu is not None else AdvancedMenu + ) + + def set_options(self) -> None: + if not path.isdir(self.kconfigs_dirname): + return + + self.input_label_txt = "Select config or action to continue (default=n)" + self.default_option = Option(method=self.select_config, opt_data=self.kconfig_default) + + self.configs = [] + option_index = 1 + for kconfig in listdir(self.kconfigs_dirname): + if not kconfig.endswith(".config"): + continue + kconfig_path = path.join(self.kconfigs_dirname, kconfig) + if path.isfile(kconfig_path): + self.configs += [kconfig] + self.options[str(option_index)] = Option(method=self.select_config, opt_data=kconfig_path) + option_index += 1 + self.options['n'] = Option(method=self.select_config, opt_data=self.kconfig_default) + + def print_menu(self) -> None: + menu = textwrap.dedent( + """ + ╟───────────────────────────────────────────────────────╢ + ║ Found previously saved firmware configs ║ + ║ ║ + ║ You can select existing firmware config or create a ║ + ║ new one. ║ + ║ ║ + """ + )[1:] + + + start_index = 1 + for i, s in enumerate(self.configs): + line = f"{start_index + i}) {s}" + menu += f"║ {line:<54}║\n" + + new_config = Color.apply("n) New firmware config", Color.YELLOW) + menu += f"║ {new_config:<63}║\n" + + menu += "║ ║\n" + menu += "╟───────────────────────────────────────────────────────╢\n" + + print(menu, end="") + + def select_config(self, **kwargs) -> None: + selection: str | None = kwargs.get("opt_data", None) + if selection is None: + raise Exception("opt_data is None") + if not path.isfile(selection) and selection != self.kconfig_default: + raise Exception("opt_data does not exists") + self.kconfig = selection + +# noinspection PyUnusedLocal +# noinspection PyMethodMayBeStatic +class KlipperBuildFirmwareMenu(BaseMenu): + def __init__(self, kconfig: str | None = None, previous_menu: Type[BaseMenu] | None = None): super().__init__() self.title = "Build Firmware Menu" self.title_color = Color.CYAN self.previous_menu: Type[BaseMenu] | None = previous_menu self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"} self.missing_deps: List[str] = check_package_install(self.deps) + self.flash_options = FlashOptions() + self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR + self.kconfig_default = path.join(KLIPPER_DIR, ".config") + self.kconfig = self.flash_options.selected_kconfig def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: from core.menus.advanced_menu import AdvancedMenu @@ -67,7 +154,7 @@ class KlipperBuildFirmwareMenu(BaseMenu): status_ok = Color.apply("*INSTALLED*", Color.GREEN) status_missing = Color.apply("*MISSING*", Color.RED) status = status_missing if d in self.missing_deps else status_ok - padding = 39 - len(d) + len(status) + (len(status_ok) - len(status)) + padding = 40 - len(d) + len(status) + (len(status_ok) - len(status)) d = Color.apply(f"● {d}", Color.CYAN) menu += f"║ {d}{status:>{padding}} ║\n" menu += "║ ║\n" @@ -98,13 +185,16 @@ class KlipperBuildFirmwareMenu(BaseMenu): def start_build_process(self, **kwargs) -> None: try: - run_make_clean() - run_make_menuconfig() - run_make() + run_make_clean(self.kconfig) + run_make_menuconfig(self.kconfig) + run_make(self.kconfig) Logger.print_ok("Firmware successfully built!") Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!") + if self.kconfig == self.kconfig_default: + self.save_firmware_config() + except Exception as e: Logger.print_error(e) Logger.print_error("Building Klipper Firmware failed!") @@ -112,3 +202,57 @@ class KlipperBuildFirmwareMenu(BaseMenu): finally: if self.previous_menu is not None: self.previous_menu().run() + + def save_firmware_config(self) -> None: + Logger.print_dialog( + DialogType.CUSTOM, + [ + "You can save the firmware build configs for multiple MCUs," + " and use them to update the firmware after a Klipper version upgrade" + ], + custom_title="Save firmware config", + ) + if not get_confirm("Do you want to save firmware config?", default_choice=False): + return + + filename = self.kconfig_default + while True: + Logger.print_dialog( + DialogType.CUSTOM, + [ + "Allowed characters: a-z, 0-9 and '-'", + "The name must not contain the following:", + "\n\n", + "● Any special characters", + "● No leading or trailing '-'", + ], + ) + input_name = get_string_input( + "Enter the new firmware config name", + regex=r"^[a-z0-9]+([a-z0-9-]*[a-z0-9])?$", + ) + filename = path.join(self.kconfigs_dirname, f"{input_name}.config") + + if path.isfile(filename): + if get_confirm(f"Firmware config {input_name} already exists, overwrite?", default_choice=False): + break + + if path.isdir(filename): + Logger.print_error(f"Path {filename} exists and it's a directory") + + if not path.exists(filename): + break + + if not get_confirm(f"Save firmware config to '{filename}'?", default_choice=True): + Logger.print_info("Aborted saving firmware config ...") + return + + if not path.exists(self.kconfigs_dirname): + mkdir(self.kconfigs_dirname) + + copyfile(self.kconfig_default, filename) + + Logger.print_ok() + Logger.print_ok(f"Firmware config successfully saved to {filename}") + + diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py index b7d741b..b9908be 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -11,6 +11,7 @@ from __future__ import annotations import textwrap import time from typing import Type +from os.path import basename from components.klipper_firmware.firmware_utils import ( find_firmware_file, @@ -36,6 +37,7 @@ from components.klipper_firmware.menus.klipper_flash_help_menu import ( KlipperFlashMethodHelpMenu, KlipperMcuConnectionHelpMenu, ) +from components.klipper_firmware.menus.klipper_build_menu import KlipperKConfigMenu from core.logger import DialogType, Logger from core.menus import FooterType, Option from core.menus.base_menu import BaseMenu, MenuTitleStyle @@ -420,6 +422,7 @@ class KlipperFlashOverviewMenu(BaseMenu): mcu = self.flash_options.selected_mcu.split("/")[-1] board = self.flash_options.selected_board baudrate = self.flash_options.selected_baudrate + kconfig = basename(self.flash_options.selected_kconfig) color = Color.CYAN subheader = f"[{Color.apply('Overview', color)}]" menu = textwrap.dedent( @@ -452,6 +455,13 @@ class KlipperFlashOverviewMenu(BaseMenu): """ )[1:] + if self.flash_options.flash_method is FlashMethod.REGULAR: + menu += textwrap.dedent( + f""" + ║ Firmware config: {Color.apply(f"{kconfig:<36}", color)} ║ + """ + )[1:] + menu += textwrap.dedent( """ ║ ║ diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py index 4c52536..ceffde6 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -14,6 +14,7 @@ from typing import Type from components.klipper import KLIPPER_DIR from components.klipper.klipper import Klipper from components.klipper_firmware.menus.klipper_build_menu import ( + KlipperKConfigMenu, KlipperBuildFirmwareMenu, ) from components.klipper_firmware.menus.klipper_flash_menu import ( @@ -76,12 +77,15 @@ class AdvancedMenu(BaseMenu): rollback_repository(MOONRAKER_DIR, Moonraker) def build(self, **kwargs) -> None: + KlipperKConfigMenu().run() KlipperBuildFirmwareMenu(previous_menu=self.__class__).run() def flash(self, **kwargs) -> None: + KlipperKConfigMenu().run() KlipperFlashMethodMenu(previous_menu=self.__class__).run() def build_flash(self, **kwargs) -> None: + KlipperKConfigMenu().run() KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run() KlipperFlashMethodMenu(previous_menu=self.__class__).run()