feature: save and select kconfig (#621)

* feature: save and select kconfig

Signed-off-by: Andrey Kozhevnikov <coderusinbox@gmail.com>

* chore: clean up and sort imports

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

* refactor: replace os.path with Pathlib

- use config paths as type Paths instead of strings.
- tweak some menu visuals.

Signed-off-by: Dominik Willner <th33xitus@gmail.com>

---------

Signed-off-by: Andrey Kozhevnikov <coderusinbox@gmail.com>
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
Co-authored-by: dw-0 <th33xitus@gmail.com>
This commit is contained in:
CODeRUS
2025-02-09 21:45:05 +07:00
committed by GitHub
parent 4978f22101
commit d8f47c0960
6 changed files with 200 additions and 13 deletions

View File

@@ -25,6 +25,7 @@ KLIPPER_SERVICE_NAME = "klipper.service"
# directories # directories
KLIPPER_DIR = Path.home().joinpath("klipper") KLIPPER_DIR = Path.home().joinpath("klipper")
KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs")
KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env")
KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups") KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups")

View File

@@ -7,6 +7,7 @@
# This file may be distributed under the terms of the GNU GPLv3 license # # This file may be distributed under the terms of the GNU GPLv3 license #
# ======================================================================= # # ======================================================================= #
import re import re
from pathlib import Path
from subprocess import ( from subprocess import (
DEVNULL, DEVNULL,
PIPE, PIPE,
@@ -138,6 +139,7 @@ def start_flash_process(flash_options: FlashOptions) -> None:
if flash_options.flash_method is FlashMethod.REGULAR: if flash_options.flash_method is FlashMethod.REGULAR:
cmd = [ cmd = [
"make", "make",
f"KCONFIG_CONFIG={flash_options.selected_kconfig}",
flash_options.flash_command.value, flash_options.flash_command.value,
f"FLASH_DEVICE={flash_options.selected_mcu}", 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") 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: try:
run( run(
"make clean", f"make KCONFIG_CONFIG={kconfig} clean",
cwd=KLIPPER_DIR, cwd=KLIPPER_DIR,
shell=True, shell=True,
check=True, check=True,
@@ -185,10 +187,10 @@ def run_make_clean() -> None:
raise raise
def run_make_menuconfig() -> None: def run_make_menuconfig(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None:
try: try:
run( run(
"make PYTHON=python3 menuconfig", f"make PYTHON=python3 KCONFIG_CONFIG={kconfig} menuconfig",
cwd=KLIPPER_DIR, cwd=KLIPPER_DIR,
shell=True, shell=True,
check=True, check=True,
@@ -198,10 +200,10 @@ def run_make_menuconfig() -> None:
raise raise
def run_make() -> None: def run_make(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None:
try: try:
run( run(
"make PYTHON=python3", f"make PYTHON=python3 KCONFIG_CONFIG={kconfig}",
cwd=KLIPPER_DIR, cwd=KLIPPER_DIR,
shell=True, shell=True,
check=True, check=True,

View File

@@ -39,6 +39,7 @@ class FlashOptions:
_selected_mcu: str = "" _selected_mcu: str = ""
_selected_board: str = "" _selected_board: str = ""
_selected_baudrate: int = 250000 _selected_baudrate: int = 250000
_selected_kconfig: str = ".config"
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if not cls._instance: if not cls._instance:
@@ -104,3 +105,11 @@ class FlashOptions:
@selected_baudrate.setter @selected_baudrate.setter
def selected_baudrate(self, value: int) -> None: def selected_baudrate(self, value: int) -> None:
self._selected_baudrate = value 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

View File

@@ -9,18 +9,22 @@
from __future__ import annotations from __future__ import annotations
import textwrap import textwrap
from pathlib import Path
from shutil import copyfile
from typing import List, Set, Type 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 ( from components.klipper_firmware.firmware_utils import (
run_make, run_make,
run_make_clean, run_make_clean,
run_make_menuconfig, 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 import Option
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from core.types.color import Color from core.types.color import Color
from utils.input_utils import get_confirm, get_string_input
from utils.sys_utils import ( from utils.sys_utils import (
check_package_install, check_package_install,
install_system_packages, install_system_packages,
@@ -30,14 +34,110 @@ from utils.sys_utils import (
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
class KlipperBuildFirmwareMenu(BaseMenu): class KlipperKConfigMenu(BaseMenu):
def __init__(self, previous_menu: Type[BaseMenu] | None = None): 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__() super().__init__()
self.title = "Build Firmware Menu" self.title = "Build Firmware Menu"
self.title_color = Color.CYAN self.title_color = Color.CYAN
self.previous_menu: Type[BaseMenu] | None = previous_menu self.previous_menu: Type[BaseMenu] | None = previous_menu
self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"} self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"}
self.missing_deps: List[str] = check_package_install(self.deps) 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: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.advanced_menu import AdvancedMenu from core.menus.advanced_menu import AdvancedMenu
@@ -67,7 +167,7 @@ class KlipperBuildFirmwareMenu(BaseMenu):
status_ok = Color.apply("*INSTALLED*", Color.GREEN) status_ok = Color.apply("*INSTALLED*", Color.GREEN)
status_missing = Color.apply("*MISSING*", Color.RED) status_missing = Color.apply("*MISSING*", Color.RED)
status = status_missing if d in self.missing_deps else status_ok 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) d = Color.apply(f"{d}", Color.CYAN)
menu += f"{d}{status:>{padding}}\n" menu += f"{d}{status:>{padding}}\n"
menu += "║ ║\n" menu += "║ ║\n"
@@ -98,13 +198,16 @@ class KlipperBuildFirmwareMenu(BaseMenu):
def start_build_process(self, **kwargs) -> None: def start_build_process(self, **kwargs) -> None:
try: try:
run_make_clean() run_make_clean(self.kconfig)
run_make_menuconfig() run_make_menuconfig(self.kconfig)
run_make() run_make(self.kconfig)
Logger.print_ok("Firmware successfully built!") Logger.print_ok("Firmware successfully built!")
Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!") 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: except Exception as e:
Logger.print_error(e) Logger.print_error(e)
Logger.print_error("Building Klipper Firmware failed!") Logger.print_error("Building Klipper Firmware failed!")
@@ -112,3 +215,62 @@ class KlipperBuildFirmwareMenu(BaseMenu):
finally: finally:
if self.previous_menu is not None: if self.previous_menu is not None:
self.previous_menu().run() 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}")

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import textwrap import textwrap
import time import time
from pathlib import Path
from typing import Type from typing import Type
from components.klipper_firmware.firmware_utils import ( from components.klipper_firmware.firmware_utils import (
@@ -420,6 +421,7 @@ class KlipperFlashOverviewMenu(BaseMenu):
mcu = self.flash_options.selected_mcu.split("/")[-1] mcu = self.flash_options.selected_mcu.split("/")[-1]
board = self.flash_options.selected_board board = self.flash_options.selected_board
baudrate = self.flash_options.selected_baudrate baudrate = self.flash_options.selected_baudrate
kconfig = Path(self.flash_options.selected_kconfig).name
color = Color.CYAN color = Color.CYAN
subheader = f"[{Color.apply('Overview', color)}]" subheader = f"[{Color.apply('Overview', color)}]"
menu = textwrap.dedent( menu = textwrap.dedent(
@@ -452,6 +454,13 @@ class KlipperFlashOverviewMenu(BaseMenu):
""" """
)[1:] )[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( menu += textwrap.dedent(
""" """
║ ║ ║ ║

View File

@@ -15,6 +15,7 @@ from components.klipper import KLIPPER_DIR
from components.klipper.klipper import Klipper from components.klipper.klipper import Klipper
from components.klipper_firmware.menus.klipper_build_menu import ( from components.klipper_firmware.menus.klipper_build_menu import (
KlipperBuildFirmwareMenu, KlipperBuildFirmwareMenu,
KlipperKConfigMenu,
) )
from components.klipper_firmware.menus.klipper_flash_menu import ( from components.klipper_firmware.menus.klipper_flash_menu import (
KlipperFlashMethodMenu, KlipperFlashMethodMenu,
@@ -76,12 +77,15 @@ class AdvancedMenu(BaseMenu):
rollback_repository(MOONRAKER_DIR, Moonraker) rollback_repository(MOONRAKER_DIR, Moonraker)
def build(self, **kwargs) -> None: def build(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperBuildFirmwareMenu(previous_menu=self.__class__).run() KlipperBuildFirmwareMenu(previous_menu=self.__class__).run()
def flash(self, **kwargs) -> None: def flash(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperFlashMethodMenu(previous_menu=self.__class__).run() KlipperFlashMethodMenu(previous_menu=self.__class__).run()
def build_flash(self, **kwargs) -> None: def build_flash(self, **kwargs) -> None:
KlipperKConfigMenu().run()
KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run() KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run()
KlipperFlashMethodMenu(previous_menu=self.__class__).run() KlipperFlashMethodMenu(previous_menu=self.__class__).run()