From d8f47c09604de0c0e15da34d7adcdf65b02cec59 Mon Sep 17 00:00:00 2001 From: CODeRUS Date: Sun, 9 Feb 2025 21:45:05 +0700 Subject: [PATCH] feature: save and select kconfig (#621) * feature: save and select kconfig Signed-off-by: Andrey Kozhevnikov * chore: clean up and sort imports Signed-off-by: Dominik Willner * refactor: replace os.path with Pathlib - use config paths as type Paths instead of strings. - tweak some menu visuals. Signed-off-by: Dominik Willner --------- Signed-off-by: Andrey Kozhevnikov Signed-off-by: Dominik Willner Co-authored-by: dw-0 --- kiauh/components/klipper/__init__.py | 1 + .../klipper_firmware/firmware_utils.py | 14 +- .../klipper_firmware/flash_options.py | 9 + .../menus/klipper_build_menu.py | 176 +++++++++++++++++- .../menus/klipper_flash_menu.py | 9 + kiauh/core/menus/advanced_menu.py | 4 + 6 files changed, 200 insertions(+), 13 deletions(-) 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 024ef63..fa3c6e8 100644 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ b/kiauh/components/klipper_firmware/firmware_utils.py @@ -7,6 +7,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license # # ======================================================================= # import re +from pathlib import Path from subprocess import ( DEVNULL, PIPE, @@ -138,6 +139,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 +174,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=Path(KLIPPER_DIR.joinpath(".config"))) -> None: try: run( - "make clean", + f"make KCONFIG_CONFIG={kconfig} clean", cwd=KLIPPER_DIR, shell=True, check=True, @@ -185,10 +187,10 @@ def run_make_clean() -> None: raise -def run_make_menuconfig() -> None: +def run_make_menuconfig(kconfig=Path(KLIPPER_DIR.joinpath(".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 +200,10 @@ def run_make_menuconfig() -> None: raise -def run_make() -> None: +def run_make(kconfig=Path(KLIPPER_DIR.joinpath(".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..60128d8 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py @@ -9,18 +9,22 @@ from __future__ import annotations import textwrap +from pathlib import Path +from shutil import copyfile from typing import List, Set, Type -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.types.color import Color +from utils.input_utils import get_confirm, get_string_input from utils.sys_utils import ( check_package_install, install_system_packages, @@ -30,14 +34,110 @@ from utils.sys_utils import ( # 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 = KLIPPER_DIR.joinpath(".config") + self.configs: List[Path] = [] + self.kconfig = ( + self.kconfig_default if not Path(self.kconfigs_dirname).is_dir() 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(self.kconfigs_dirname).is_dir(): + 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 + ) + + option_index = 1 + for kconfig in Path(self.kconfigs_dirname).iterdir(): + if not kconfig.name.endswith(".config"): + continue + kconfig_path = self.kconfigs_dirname.joinpath(kconfig) + if Path(kconfig_path).is_file(): + 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: + cfg_found_str = Color.apply( + "Previously saved firmware configs found!", Color.GREEN + ) + menu = textwrap.dedent( + f""" + ╟───────────────────────────────────────────────────────╢ + ║ {cfg_found_str:^62} ║ + ║ ║ + ║ Select an existing config or create a new one. ║ + ╟───────────────────────────────────────────────────────╢ + ║ Available firmware configs: ║ + """ + )[1:] + + start_index = 1 + for i, s in enumerate(self.configs): + line = f"{start_index + i}) {s.name}" + menu += f"║ {line:<54}║\n" + + new_config = Color.apply("N) Create new firmware config", Color.GREEN) + menu += "║ ║\n" + menu += f"║ {new_config:<62} ║\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(selection).is_file() 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 = KLIPPER_DIR.joinpath(".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 +167,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 +198,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 +215,62 @@ 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 = self.kconfigs_dirname.joinpath(f"{input_name}.config") + + if Path(filename).is_file(): + if get_confirm( + f"Firmware config {input_name} already exists, overwrite?", + default_choice=False, + ): + break + + if Path(filename).is_dir(): + Logger.print_error(f"Path {filename} exists and it's a directory") + + if not Path(filename).exists(): + 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(self.kconfigs_dirname).exists(): + Path(self.kconfigs_dirname).mkdir() + + 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..00d0765 100644 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py @@ -10,6 +10,7 @@ from __future__ import annotations import textwrap import time +from pathlib import Path from typing import Type from components.klipper_firmware.firmware_utils import ( @@ -420,6 +421,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 = Path(self.flash_options.selected_kconfig).name color = Color.CYAN subheader = f"[{Color.apply('Overview', color)}]" menu = textwrap.dedent( @@ -452,6 +454,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..22cf641 100644 --- a/kiauh/core/menus/advanced_menu.py +++ b/kiauh/core/menus/advanced_menu.py @@ -15,6 +15,7 @@ from components.klipper import KLIPPER_DIR from components.klipper.klipper import Klipper from components.klipper_firmware.menus.klipper_build_menu import ( KlipperBuildFirmwareMenu, + KlipperKConfigMenu, ) from components.klipper_firmware.menus.klipper_flash_menu import ( KlipperFlashMethodMenu, @@ -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()