Compare commits

...

2 Commits

Author SHA1 Message Date
dw-0
bb769fdf6d fix: hitting 'b' or 'h' in main menu raises exception
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-04-10 22:49:52 +02:00
dw-0
409aa3da25 refactor: extend firmware flashing functionalities
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-04-10 21:10:01 +02:00
10 changed files with 526 additions and 188 deletions

View File

@@ -0,0 +1,12 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 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 components.klipper import KLIPPER_DIR
SD_FLASH_SCRIPT = KLIPPER_DIR.joinpath("scripts/flash-sdcard.sh")

View File

@@ -13,8 +13,8 @@ from typing import Union, List
class FlashMethod(Enum): class FlashMethod(Enum):
REGULAR = "REGULAR" REGULAR = "Regular"
SD_CARD = "SD_CARD" SD_CARD = "SD Card"
class FlashCommand(Enum): class FlashCommand(Enum):
@@ -24,7 +24,7 @@ class FlashCommand(Enum):
class ConnectionType(Enum): class ConnectionType(Enum):
USB = "USB" USB = "USB"
USB_DFU = "USB_DFU" USB_DFU = "USB (DFU)"
UART = "UART" UART = "UART"
@@ -36,6 +36,7 @@ class FlashOptions:
_mcu_list: List[str] = field(default_factory=list) _mcu_list: List[str] = field(default_factory=list)
_selected_mcu: str = "" _selected_mcu: str = ""
_selected_board: str = "" _selected_board: str = ""
_selected_baudrate: int = 250000
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if not cls._instance: if not cls._instance:
@@ -93,3 +94,11 @@ class FlashOptions:
@selected_board.setter @selected_board.setter
def selected_board(self, value: str) -> None: def selected_board(self, value: str) -> None:
self._selected_board = value self._selected_board = value
@property
def selected_baudrate(self) -> int:
return self._selected_baudrate
@selected_baudrate.setter
def selected_baudrate(self, value: int) -> None:
self._selected_baudrate = value

View File

@@ -7,15 +7,35 @@
# 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 subprocess
from subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT from subprocess import CalledProcessError, check_output, Popen, PIPE, STDOUT
from typing import List from typing import List
from components.klipper import KLIPPER_DIR from components.klipper import KLIPPER_DIR
from components.klipper_firmware.flash_options import FlashOptions, FlashCommand from components.klipper_firmware import SD_FLASH_SCRIPT
from components.klipper_firmware.flash_options import (
FlashOptions,
FlashMethod,
)
from utils.logger import Logger from utils.logger import Logger
from utils.system_utils import log_process from utils.system_utils import log_process
def find_firmware_file(method: FlashMethod) -> bool:
target = KLIPPER_DIR.joinpath("out")
target_exists = target.exists()
if method is FlashMethod.REGULAR:
f1 = "klipper.elf.hex"
f2 = "klipper.elf"
fw_file_exists = target.joinpath(f1).exists() and target.joinpath(f2).exists()
elif method is FlashMethod.SD_CARD:
fw_file_exists = target.joinpath("klipper.bin").exists()
else:
raise Exception("Unknown flash method")
return target_exists and fw_file_exists
def find_usb_device_by_id() -> List[str]: def find_usb_device_by_id() -> List[str]:
try: try:
command = "find /dev/serial/by-id/* 2>/dev/null" command = "find /dev/serial/by-id/* 2>/dev/null"
@@ -49,21 +69,55 @@ def find_usb_dfu_device() -> List[str]:
return [] return []
def flash_device(flash_options: FlashOptions) -> None: def get_sd_flash_board_list() -> List[str]:
if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists():
return []
try: try:
cmd = f"{SD_FLASH_SCRIPT} -l"
blist = subprocess.check_output(cmd, shell=True, text=True)
return blist.splitlines()[1:]
except subprocess.CalledProcessError as e:
Logger.print_error(f"An unexpected error occured:\n{e}")
def start_flash_process(flash_options: FlashOptions) -> None:
Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...")
try:
if not flash_options.flash_method:
raise Exception("Missing value for flash_method!")
if not flash_options.flash_command:
raise Exception("Missing value for flash_command!")
if not flash_options.selected_mcu: if not flash_options.selected_mcu:
raise Exception("Missing value for selected_mcu!") raise Exception("Missing value for selected_mcu!")
if not flash_options.connection_type:
raise Exception("Missing value for connection_type!")
if (
flash_options.flash_method == FlashMethod.SD_CARD
and not flash_options.selected_board
):
raise Exception("Missing value for selected_board!")
if flash_options.flash_command is FlashCommand.FLASH: if flash_options.flash_method is FlashMethod.REGULAR:
command = [ cmd = [
"make", "make",
flash_options.flash_command.value, flash_options.flash_command.value,
f"FLASH_DEVICE={flash_options.selected_mcu}", f"FLASH_DEVICE={flash_options.selected_mcu}",
] ]
process = Popen( elif flash_options.flash_method is FlashMethod.SD_CARD:
command, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True if not SD_FLASH_SCRIPT.exists():
) raise Exception("Unable to find Klippers sdcard flash script!")
cmd = [
SD_FLASH_SCRIPT,
"-b",
flash_options.selected_baudrate,
flash_options.selected_mcu,
flash_options.selected_board,
]
else:
raise Exception("Invalid value for flash_method!")
process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True)
log_process(process) log_process(process)
rc = process.returncode rc = process.returncode

View File

@@ -0,0 +1,97 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 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 textwrap
from components.klipper_firmware.flash_options import FlashOptions, FlashMethod
from core.menus import FooterType
from core.menus.base_menu import BaseMenu
from utils.constants import COLOR_RED, RESET_FORMAT
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class KlipperNoFirmwareErrorMenu(BaseMenu):
def __init__(self):
super().__init__()
self.flash_options = FlashOptions()
self.default_option = self.go_back
self.footer_type = FooterType.BLANK
self.input_label_txt = "Press ENTER to go back to [Advanced Menu]"
def print_menu(self) -> None:
header = "!!! NO FIRMWARE FILE FOUND !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {line1:<62} |
| |
| Make sure, that: |
| ● the folder '~/klipper/out' and its content exist |
| ● the folder contains the following file: |
"""
)[1:]
if self.flash_options.flash_method is FlashMethod.REGULAR:
menu += "| ● 'klipper.elf' |\n"
menu += "| ● 'klipper.elf.hex' |\n"
else:
menu += "| ● 'klipper.bin' |\n"
print(menu, end="")
def go_back(self, **kwargs) -> None:
from core.menus.advanced_menu import AdvancedMenu
AdvancedMenu().run()
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class KlipperNoBoardTypesErrorMenu(BaseMenu):
def __init__(self):
super().__init__()
self.default_option = self.go_back
self.footer_type = FooterType.BLANK
self.input_label_txt = "Press ENTER to go back to [Main Menu]"
def print_menu(self) -> None:
header = "!!! ERROR GETTING BOARD LIST !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {line1:<62} |
| |
| Make sure, that: |
| ● the folder '~/klipper' and all its content exist |
| ● the content of folder '~/klipper' is not currupted |
| ● the file '~/klipper/scripts/flash-sd.py' exist |
| ● your current user has access to those files/folders |
| |
| If in doubt or this process continues to fail, please |
| consider to download Klipper again. |
"""
)[1:]
print(menu, end="")
def go_back(self, **kwargs) -> None:
from core.menus.main_menu import MainMenu
MainMenu().run()

View File

@@ -0,0 +1,127 @@
# ======================================================================= #
# Copyright (C) 2020 - 2024 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 textwrap
from core.menus.base_menu import BaseMenu
from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW
class KlipperFlashMethodHelpMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu):
super().__init__()
self.previous_menu: BaseMenu = previous_menu
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {subheader1:<62} |
| The default method to flash controller boards which |
| are connected and updated over USB and not by placing |
| a compiled firmware file onto an internal SD-Card. |
| |
| Common controllers that get flashed that way are: |
| - Arduino Mega 2560 |
| - Fysetc F6 / S6 (used without a Display + SD-Slot) |
| |
| {subheader2:<62} |
| Many popular controller boards ship with a bootloader |
| capable of updating the firmware via SD-Card. |
| Choose this method if your controller board supports |
| this way of updating. This method ONLY works for up- |
| grading firmware. The initial flashing procedure must |
| be done manually per the instructions that apply to |
| your controller board. |
| |
| Common controllers that can be flashed that way are: |
| - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 |
| - Fysetc F6 / S6 (used with a Display + SD-Slot) |
| - Fysetc Spider |
| |
"""
)[1:]
print(menu, end="")
class KlipperFlashCommandHelpMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu):
super().__init__()
self.previous_menu: BaseMenu = previous_menu
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {subheader1:<62} |
| The default command to flash controller board, it |
| will detect selected microcontroller and use suitable |
| tool for flashing it. |
| |
| {subheader2:<62} |
| Special command to flash STM32 microcontrollers in |
| DFU mode but connected via serial. stm32flash command |
| will be used internally. |
| |
"""
)[1:]
print(menu, end="")
class KlipperMcuConnectionHelpMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu):
super().__init__()
self.previous_menu: BaseMenu = previous_menu
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {subheader1:<62} |
| Selecting USB as the connection method will scan the |
| USB ports for connected controller boards. This will |
| be similar to the 'ls /dev/serial/by-id/*' command |
| suggested by the official Klipper documentation for |
| determining successfull USB connections! |
| |
| {subheader2:<62} |
| Selecting UART as the connection method will list all |
| possible UART serial ports. Note: This method ALWAYS |
| returns something as it seems impossible to determine |
| if a valid Klipper controller board is connected or |
| not. Because of that, you MUST know which UART serial |
| port your controller board is connected to when using |
| this connection method. |
| |
"""
)[1:]
print(menu, end="")

View File

@@ -20,13 +20,24 @@ from components.klipper_firmware.flash_utils import (
find_usb_device_by_id, find_usb_device_by_id,
find_uart_device, find_uart_device,
find_usb_dfu_device, find_usb_dfu_device,
flash_device, get_sd_flash_board_list,
start_flash_process,
find_firmware_file,
)
from components.klipper_firmware.menus.klipper_flash_error_menu import (
KlipperNoBoardTypesErrorMenu,
KlipperNoFirmwareErrorMenu,
)
from components.klipper_firmware.menus.klipper_flash_help_menu import (
KlipperMcuConnectionHelpMenu,
KlipperFlashCommandHelpMenu,
KlipperFlashMethodHelpMenu,
) )
from core.menus import FooterType from core.menus import FooterType
from core.menus.base_menu import BaseMenu from core.menus.base_menu import BaseMenu
from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED from utils.constants import COLOR_CYAN, RESET_FORMAT, COLOR_YELLOW, COLOR_RED
from utils.input_utils import get_confirm from utils.input_utils import get_number_input
from utils.logger import Logger from utils.logger import Logger
@@ -37,10 +48,10 @@ class KlipperFlashMethodMenu(BaseMenu):
super().__init__() super().__init__()
self.previous_menu: BaseMenu = previous_menu self.previous_menu: BaseMenu = previous_menu
self.help_menu = KlipperFlashMethodHelpMenu
self.options = { self.options = {
"1": self.select_regular, "1": self.select_regular,
"2": self.select_sdcard, "2": self.select_sdcard,
"h": self.help_menu,
} }
self.input_label_txt = "Select flash method" self.input_label_txt = "Select flash method"
self.footer_type = FooterType.BACK_HELP self.footer_type = FooterType.BACK_HELP
@@ -48,7 +59,11 @@ class KlipperFlashMethodMenu(BaseMenu):
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
def print_menu(self) -> None: def print_menu(self) -> None:
header = " [ Flash MCU ] " header = " [ MCU Flash Menu ] "
subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}"
subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}"
subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}"
color = COLOR_CYAN color = COLOR_CYAN
count = 62 - len(color) - len(RESET_FORMAT) count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
@@ -56,9 +71,11 @@ class KlipperFlashMethodMenu(BaseMenu):
/=======================================================\\ /=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} | | {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------| |-------------------------------------------------------|
| Please select the flashing method to flash your MCU. | | Select the flash method for flashing the MCU. |
| Make sure to only select a method your MCU supports. | | |
| Not all MCUs support both methods! | | {subheader:<62} |
| {subline1:<62} |
| {subline2:<62} |
|-------------------------------------------------------| |-------------------------------------------------------|
| | | |
| 1) Regular flashing method | | 1) Regular flashing method |
@@ -77,10 +94,10 @@ class KlipperFlashMethodMenu(BaseMenu):
self.goto_next_menu() self.goto_next_menu()
def goto_next_menu(self, **kwargs): def goto_next_menu(self, **kwargs):
if find_firmware_file(self.flash_options.flash_method):
KlipperFlashCommandMenu(previous_menu=self).run() KlipperFlashCommandMenu(previous_menu=self).run()
else:
def help_menu(self, **kwargs): KlipperNoFirmwareErrorMenu().run()
KlipperFlashMethodHelpMenu(previous_menu=self).run()
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@@ -90,10 +107,10 @@ class KlipperFlashCommandMenu(BaseMenu):
super().__init__() super().__init__()
self.previous_menu: BaseMenu = previous_menu self.previous_menu: BaseMenu = previous_menu
self.help_menu = KlipperFlashCommandHelpMenu
self.options = { self.options = {
"1": self.select_flash, "1": self.select_flash,
"2": self.select_serialflash, "2": self.select_serialflash,
"h": self.help_menu,
} }
self.default_option = self.select_flash self.default_option = self.select_flash
self.input_label_txt = "Select flash command" self.input_label_txt = "Select flash command"
@@ -125,9 +142,6 @@ class KlipperFlashCommandMenu(BaseMenu):
def goto_next_menu(self, **kwargs): def goto_next_menu(self, **kwargs):
KlipperSelectMcuConnectionMenu(previous_menu=self).run() KlipperSelectMcuConnectionMenu(previous_menu=self).run()
def help_menu(self, **kwargs):
KlipperFlashCommandHelpMenu(previous_menu=self).run()
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
@@ -137,11 +151,11 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
self.__standalone = standalone self.__standalone = standalone
self.previous_menu: BaseMenu = previous_menu self.previous_menu: BaseMenu = previous_menu
self.help_menu = KlipperMcuConnectionHelpMenu
self.options = { self.options = {
"1": self.select_usb, "1": self.select_usb,
"2": self.select_dfu, "2": self.select_dfu,
"3": self.select_usb_dfu, "3": self.select_usb_dfu,
"h": self.help_menu,
} }
self.input_label_txt = "Select connection type" self.input_label_txt = "Select connection type"
self.footer_type = FooterType.BACK_HELP self.footer_type = FooterType.BACK_HELP
@@ -209,9 +223,6 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
def goto_next_menu(self, **kwargs): def goto_next_menu(self, **kwargs):
KlipperSelectMcuIdMenu(previous_menu=self).run() KlipperSelectMcuIdMenu(previous_menu=self).run()
def help_menu(self, **kwargs):
KlipperMcuConnectionHelpMenu(previous_menu=self).run()
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
@@ -256,132 +267,139 @@ class KlipperSelectMcuIdMenu(BaseMenu):
selected_mcu = self.mcu_list[index] selected_mcu = self.mcu_list[index]
self.flash_options.selected_mcu = selected_mcu self.flash_options.selected_mcu = selected_mcu
print(f"{COLOR_CYAN}###### You selected:{RESET_FORMAT}") if self.flash_options.flash_method == FlashMethod.SD_CARD:
print(f"● MCU #{index}: {selected_mcu}\n") KlipperSelectSDFlashBoardMenu(previous_menu=self).run()
elif self.flash_options.flash_method == FlashMethod.REGULAR:
KlipperFlashOverviewMenu(previous_menu=self).run()
if get_confirm("Continue", allow_go_back=True):
Logger.print_status(f"Flashing '{selected_mcu}' ...")
flash_device(self.flash_options)
self.goto_next_menu() # noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class KlipperSelectSDFlashBoardMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu):
super().__init__()
def goto_next_menu(self, **kwargs): self.previous_menu: BaseMenu = previous_menu
from core.menus.main_menu import MainMenu self.flash_options = FlashOptions()
self.available_boards = get_sd_flash_board_list()
self.input_label_txt = "Select board type"
options = {f"{i}": self.board_select for i in range(len(self.available_boards))}
self.options = options
def print_menu(self) -> None:
if len(self.available_boards) < 1:
KlipperNoBoardTypesErrorMenu().run()
else:
menu = textwrap.dedent(
"""
/=======================================================\\
| Please select the type of board that corresponds to |
| the currently selected MCU ID you chose before. |
| |
| The following boards are currently supported: |
|-------------------------------------------------------|
"""
)[1:]
for i, board in enumerate(self.available_boards):
line = f" {i}) {board}"
menu += f"|{line:<55}|\n"
print(menu, end="")
def board_select(self, **kwargs):
board = int(kwargs.get("opt_index"))
self.flash_options.selected_board = self.available_boards[board]
self.baudrate_select()
def baudrate_select(self, **kwargs):
menu = textwrap.dedent(
"""
/=======================================================\\
| If your board is flashed with firmware that connects |
| at a custom baud rate, please change it now. |
| |
| If you are unsure, stick to the default 250000! |
\\=======================================================/
"""
)[1:]
print(menu, end="")
self.flash_options.selected_baudrate = get_number_input(
question="Please set the baud rate",
default=250000,
min_count=0,
allow_go_back=True,
)
KlipperFlashOverviewMenu(previous_menu=self).run()
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class KlipperFlashOverviewMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu):
super().__init__()
self.previous_menu: BaseMenu = previous_menu
self.flash_options = FlashOptions()
self.options = {"Y": self.execute_flash, "N": self.abort_process}
self.input_label_txt = "Perform action (default=Y)"
self.default_option = self.execute_flash
def print_menu(self) -> None:
header = "!!! ATTENTION !!!"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
method = self.flash_options.flash_method.value
command = self.flash_options.flash_command.value
conn_type = self.flash_options.connection_type.value
mcu = self.flash_options.selected_mcu
board = self.flash_options.selected_board
baudrate = self.flash_options.selected_baudrate
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| Before contuining the flashing process, please check |
| if all parameters were set correctly! Once you made |
| sure everything is correct, start the process. If any |
| parameter needs to be changed, you can go back (B) |
| step by step or abort and start from the beginning. |
|{subheader:-^64}|
"""
)[1:]
menu += f" ● MCU: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n"
menu += f" ● Connection: {COLOR_CYAN}{conn_type}{RESET_FORMAT}\n"
menu += f" ● Flash method: {COLOR_CYAN}{method}{RESET_FORMAT}\n"
menu += f" ● Flash command: {COLOR_CYAN}{command}{RESET_FORMAT}\n"
if self.flash_options.flash_method is FlashMethod.SD_CARD:
menu += f" ● Board type: {COLOR_CYAN}{board}{RESET_FORMAT}\n"
menu += f" ● Baudrate: {COLOR_CYAN}{baudrate}{RESET_FORMAT}\n"
menu += textwrap.dedent(
"""
|-------------------------------------------------------|
| Y) Start flash process |
| N) Abort - Return to Advanced Menu |
"""
)
print(menu, end="")
def execute_flash(self, **kwargs):
from core.menus.advanced_menu import AdvancedMenu from core.menus.advanced_menu import AdvancedMenu
AdvancedMenu(previous_menu=MainMenu()).run() start_flash_process(self.flash_options)
Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...")
time.sleep(5)
KlipperFlashMethodMenu(previous_menu=AdvancedMenu()).run()
def abort_process(self, **kwargs):
from core.menus.advanced_menu import AdvancedMenu
class KlipperFlashMethodHelpMenu(BaseMenu): AdvancedMenu().run()
def __init__(self, previous_menu: BaseMenu):
super().__init__()
self.previous_menu: BaseMenu = previous_menu
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {subheader1:<62} |
| The default method to flash controller boards which |
| are connected and updated over USB and not by placing |
| a compiled firmware file onto an internal SD-Card. |
| |
| Common controllers that get flashed that way are: |
| - Arduino Mega 2560 |
| - Fysetc F6 / S6 (used without a Display + SD-Slot) |
| |
| {subheader2:<62} |
| Many popular controller boards ship with a bootloader |
| capable of updating the firmware via SD-Card. |
| Choose this method if your controller board supports |
| this way of updating. This method ONLY works for up- |
| grading firmware. The initial flashing procedure must |
| be done manually per the instructions that apply to |
| your controller board. |
| |
| Common controllers that can be flashed that way are: |
| - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 |
| - Fysetc F6 / S6 (used with a Display + SD-Slot) |
| - Fysetc Spider |
| |
"""
)[1:]
print(menu, end="")
class KlipperFlashCommandHelpMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu):
super().__init__()
self.previous_menu: BaseMenu = previous_menu
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {subheader1:<62} |
| The default command to flash controller board, it |
| will detect selected microcontroller and use suitable |
| tool for flashing it. |
| |
| {subheader2:<62} |
| Special command to flash STM32 microcontrollers in |
| DFU mode but connected via serial. stm32flash command |
| will be used internally. |
| |
"""
)[1:]
print(menu, end="")
class KlipperMcuConnectionHelpMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu):
super().__init__()
self.previous_menu: BaseMenu = previous_menu
def print_menu(self) -> None:
header = " < ? > Help: Flash MCU < ? > "
color = COLOR_YELLOW
count = 62 - len(color) - len(RESET_FORMAT)
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}"
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
menu = textwrap.dedent(
f"""
/=======================================================\\
| {color}{header:~^{count}}{RESET_FORMAT} |
|-------------------------------------------------------|
| {subheader1:<62} |
| Selecting USB as the connection method will scan the |
| USB ports for connected controller boards. This will |
| be similar to the 'ls /dev/serial/by-id/*' command |
| suggested by the official Klipper documentation for |
| determining successfull USB connections! |
| |
| {subheader2:<62} |
| Selecting UART as the connection method will list all |
| possible UART serial ports. Note: This method ALWAYS |
| returns something as it seems impossible to determine |
| if a valid Klipper controller board is connected or |
| not. Because of that, you MUST know which UART serial |
| port your controller board is connected to when using |
| this connection method. |
| |
"""
)[1:]
print(menu, end="")

View File

@@ -14,13 +14,7 @@ class FooterType(Enum):
QUIT = "QUIT" QUIT = "QUIT"
BACK = "BACK" BACK = "BACK"
BACK_HELP = "BACK_HELP" BACK_HELP = "BACK_HELP"
BLANK = "BLANK"
NAVI_OPTIONS = {
FooterType.QUIT: ["q"],
FooterType.BACK: ["b"],
FooterType.BACK_HELP: ["b", "h"],
}
class ExitAppException(Exception): class ExitAppException(Exception):

View File

@@ -19,10 +19,12 @@ from utils.constants import COLOR_YELLOW, RESET_FORMAT
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class AdvancedMenu(BaseMenu): class AdvancedMenu(BaseMenu):
def __init__(self, previous_menu: BaseMenu): def __init__(self):
super().__init__() super().__init__()
self.previous_menu: BaseMenu = previous_menu from core.menus.main_menu import MainMenu
self.previous_menu: BaseMenu = MainMenu()
self.options = { self.options = {
"1": None, "1": None,
"2": None, "2": None,

View File

@@ -12,10 +12,10 @@ from __future__ import annotations
import subprocess import subprocess
import sys import sys
import textwrap import textwrap
from abc import abstractmethod, ABC from abc import abstractmethod
from typing import Dict, Union, Callable, Type, Tuple from typing import Dict, Union, Callable, Type, Tuple
from core.menus import FooterType, NAVI_OPTIONS, ExitAppException, GoBackException from core.menus import FooterType, ExitAppException, GoBackException
from utils.constants import ( from utils.constants import (
COLOR_GREEN, COLOR_GREEN,
COLOR_YELLOW, COLOR_YELLOW,
@@ -92,22 +92,55 @@ def print_back_help_footer():
print(footer, end="") print(footer, end="")
Options = Dict[str, Callable] def print_blank_footer():
print("\=======================================================/")
class BaseMenu(ABC): class PostInitCaller(type):
options: Options = {} def __call__(cls, *args, **kwargs):
obj = type.__call__(cls, *args, **kwargs)
obj.__post_init__()
return obj
# noinspection PyUnusedLocal
# noinspection PyMethodMayBeStatic
class BaseMenu(metaclass=PostInitCaller):
options: Dict[str, Callable] = {}
options_offset: int = 0 options_offset: int = 0
default_option: Union[Callable, None] = None default_option: Union[Callable, None] = None
input_label_txt: str = "Perform action" input_label_txt: str = "Perform action"
header: bool = False header: bool = False
previous_menu: Union[Type[BaseMenu], BaseMenu] = None previous_menu: Union[Type[BaseMenu], BaseMenu] = None
help_menu: Type[BaseMenu] = None
footer_type: FooterType = FooterType.BACK footer_type: FooterType = FooterType.BACK
def __init__(self): def __init__(self, **kwargs):
if type(self) is BaseMenu: if type(self) is BaseMenu:
raise NotImplementedError("BaseMenu cannot be instantiated directly.") raise NotImplementedError("BaseMenu cannot be instantiated directly.")
def __post_init__(self):
# conditionally add options based on footer type
if self.footer_type is FooterType.QUIT:
self.options["q"] = self.__exit
if self.footer_type is FooterType.BACK:
self.options["b"] = self.__go_back
if self.footer_type is FooterType.BACK_HELP:
self.options["b"] = self.__go_back
self.options["h"] = self.__go_to_help
# if defined, add the default option to the options dict
if self.default_option is not None:
self.options[""] = self.default_option
def __go_back(self, **kwargs):
raise GoBackException()
def __go_to_help(self, **kwargs):
self.help_menu(previous_menu=self).run()
def __exit(self, **kwargs):
raise ExitAppException()
@abstractmethod @abstractmethod
def print_menu(self) -> None: def print_menu(self) -> None:
raise NotImplementedError("Subclasses must implement the print_menu method") raise NotImplementedError("Subclasses must implement the print_menu method")
@@ -119,6 +152,8 @@ class BaseMenu(ABC):
print_back_footer() print_back_footer()
elif self.footer_type is FooterType.BACK_HELP: elif self.footer_type is FooterType.BACK_HELP:
print_back_help_footer() print_back_help_footer()
elif self.footer_type is FooterType.BLANK:
print_blank_footer()
else: else:
raise NotImplementedError("Method for printing footer not implemented.") raise NotImplementedError("Method for printing footer not implemented.")
@@ -138,20 +173,8 @@ class BaseMenu(ABC):
usr_input = usr_input.lower() usr_input = usr_input.lower()
option = self.options.get(usr_input, None) option = self.options.get(usr_input, None)
# check if usr_input contains a character used for basic navigation, e.g. b, h or q # if option/usr_input is None/empty string, we execute the menus default option if specified
# and if the current menu has the appropriate footer to allow for that action if (option is None or usr_input == "") and self.default_option is not None:
is_valid_navigation = self.footer_type in NAVI_OPTIONS
user_navigated = usr_input in NAVI_OPTIONS[self.footer_type]
if is_valid_navigation and user_navigated:
if usr_input == "q":
raise ExitAppException()
elif usr_input == "b":
raise GoBackException()
elif usr_input == "h":
return option, usr_input
# if usr_input is None or an empty string, we execute the menues default option if specified
if usr_input == "" and self.default_option is not None:
return self.default_option, usr_input return self.default_option, usr_input
# user selected a regular option # user selected a regular option
@@ -162,8 +185,10 @@ class BaseMenu(ABC):
while True: while True:
print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="") print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="")
usr_input = input().lower() usr_input = input().lower()
validated_input = self.validate_user_input(usr_input)
method_to_call = validated_input[0]
if (validated_input := self.validate_user_input(usr_input)) is not None: if method_to_call is not None:
return validated_input return validated_input
else: else:
Logger.print_error("Invalid input!", False) Logger.print_error("Invalid input!", False)

View File

@@ -152,7 +152,7 @@ class MainMenu(BaseMenu):
RemoveMenu(previous_menu=self).run() RemoveMenu(previous_menu=self).run()
def advanced_menu(self, **kwargs): def advanced_menu(self, **kwargs):
AdvancedMenu(previous_menu=self).run() AdvancedMenu().run()
def backup_menu(self, **kwargs): def backup_menu(self, **kwargs):
BackupMenu(previous_menu=self).run() BackupMenu(previous_menu=self).run()