Merge pull request #511 from dw-0/develop

Merge develop into master
This commit is contained in:
dw-0
2024-09-01 19:02:48 +02:00
committed by GitHub
10 changed files with 107 additions and 60 deletions

View File

@@ -81,6 +81,7 @@ class KlipperBuildFirmwareMenu(BaseMenu):
line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}"
menu += f"{line:<62}\n"
menu += "╟───────────────────────────────────────────────────────╢\n"
print(menu, end="")

View File

@@ -249,7 +249,7 @@ class KlipperSelectMcuIdMenu(BaseMenu):
self.flash_options = FlashOptions()
self.mcu_list = self.flash_options.mcu_list
self.input_label_txt = "Select MCU to flash"
self.footer_type = FooterType.BACK_HELP
self.footer_type = FooterType.BACK
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
self.previous_menu = (
@@ -265,7 +265,7 @@ class KlipperSelectMcuIdMenu(BaseMenu):
def print_menu(self) -> None:
header = "!!! ATTENTION !!!"
header2 = f"[{COLOR_CYAN}List of available MCUs{RESET_FORMAT}]"
header2 = f"[{COLOR_CYAN}List of detected MCUs{RESET_FORMAT}]"
color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent(
@@ -277,15 +277,21 @@ class KlipperSelectMcuIdMenu(BaseMenu):
║ ONLY flash a firmware created for the respective MCU! ║
║ ║
{header2:─^64}
║ ║
"""
)[1:]
for i, mcu in enumerate(self.mcu_list):
mcu = mcu.split("/")[-1]
menu += f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n"
menu += "╟───────────────────────────┬───────────────────────────╢"
menu += f" {i}) {COLOR_CYAN}{mcu:<51}{RESET_FORMAT}\n"
print(menu, end="\n")
menu += textwrap.dedent(
"""
║ ║
╟───────────────────────────────────────────────────────╢
"""
)[1:]
print(menu, end="")
def flash_mcu(self, **kwargs):
try:
@@ -343,8 +349,8 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
for i, board in enumerate(self.available_boards):
line = f" {i}) {board}"
menu += f"|{line:<55}|\n"
menu += f"{line:<55}\n"
menu += "╟───────────────────────────────────────────────────────╢"
print(menu, end="")
def board_select(self, **kwargs):
@@ -392,8 +398,8 @@ class KlipperFlashOverviewMenu(BaseMenu):
def set_options(self) -> None:
self.options = {
"Y": Option(self.execute_flash),
"N": Option(self.abort_process),
"y": Option(self.execute_flash),
"n": Option(self.abort_process),
}
self.default_option = Option(self.execute_flash)
@@ -406,7 +412,7 @@ class KlipperFlashOverviewMenu(BaseMenu):
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
mcu = self.flash_options.selected_mcu.split("/")[-1]
board = self.flash_options.selected_board
baudrate = self.flash_options.selected_baudrate
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]"
@@ -420,26 +426,37 @@ class KlipperFlashOverviewMenu(BaseMenu):
║ 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}
{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"
menu += textwrap.dedent(
f"""
║ MCU: {COLOR_CYAN}{mcu:<48}{RESET_FORMAT}
║ Connection: {COLOR_CYAN}{conn_type:<41}{RESET_FORMAT}
║ Flash method: {COLOR_CYAN}{method:<39}{RESET_FORMAT}
║ Flash command: {COLOR_CYAN}{command:<38}{RESET_FORMAT}
"""
)[1:]
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(
f"""
║ Board type: {COLOR_CYAN}{board:<41}{RESET_FORMAT}
║ Baudrate: {COLOR_CYAN}{baudrate:<43}{RESET_FORMAT}
"""
)[1:]
menu += textwrap.dedent(
"""
║ ║
╟───────────────────────────────────────────────────────╢
║ Y) Start flash process ║
║ N) Abort - Return to Advanced Menu ║
╟───────────────────────────────────────────────────────╢
"""
)
)[1:]
print(menu, end="")
def execute_flash(self, **kwargs):

View File

@@ -22,7 +22,10 @@ class Option:
:param opt_data: Can be used to pass any additional data to the menu option
"""
method: Type[Callable] | None = None
def __repr__(self):
return f"Option(method={self.method.__name__}, opt_index={self.opt_index}, opt_data={self.opt_data})"
method: Type[Callable]
opt_index: str = ""
opt_data: Any = None

View File

@@ -25,6 +25,7 @@ from core.constants import (
)
from core.logger import Logger
from core.menus import FooterType, Option
from utils.input_utils import get_selection_input
def clear() -> None:
@@ -141,7 +142,7 @@ class BaseMenu(metaclass=PostInitCaller):
def __go_to_help(self, **kwargs) -> None:
if self.help_menu is None:
return
self.help_menu(previous_menu=self).run()
self.help_menu(previous_menu=self.__class__).run()
def __exit(self, **kwargs) -> None:
Logger.print_ok("###### Happy printing!", False)
@@ -177,46 +178,20 @@ class BaseMenu(metaclass=PostInitCaller):
self.print_menu()
self.print_footer()
def validate_user_input(self, usr_input: str) -> Option:
"""
Validate the user input and either return an Option, a string or None
:param usr_input: The user input in form of a string
:return: Option, str or None
"""
usr_input = usr_input.lower()
option = self.options.get(
usr_input,
Option(method=None, opt_index="", opt_data=None),
)
# if option/usr_input is None/empty string, we execute the menus default option if specified
if (option is None or usr_input == "") and self.default_option is not None:
self.default_option.opt_index = usr_input
return self.default_option
# user selected a regular option
option.opt_index = usr_input
return option
def handle_user_input(self) -> Option:
"""Handle the user input, return the validated input or print an error."""
while True:
print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="")
usr_input = input().lower()
validated_input = self.validate_user_input(usr_input)
if validated_input.method is not None:
return validated_input
else:
Logger.print_error("Invalid input!", False)
def run(self) -> None:
"""Start the menu lifecycle. When this function returns, the lifecycle of the menu ends."""
try:
self.display_menu()
option = self.handle_user_input()
option.method(opt_index=option.opt_index, opt_data=option.opt_data)
option = get_selection_input(self.input_label_txt, self.options)
selected_option: Option = self.options.get(option)
selected_option.method(
opt_index=selected_option.opt_index,
opt_data=selected_option.opt_data,
)
self.run()
except Exception as e:
Logger.print_error(
f"An unexpected error occured:\n{e}\n{traceback.format_exc()}"

View File

@@ -85,10 +85,46 @@ class DuplicateOptionError(Exception):
class SimpleConfigParser:
"""A customized config parser targeted at handling Klipper style config files"""
_SECTION_RE = re.compile(r"\s*\[(\w+\s?.+)]\s*([#;].*)?$")
_OPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([^=:].*)\s*([#;].*)?$")
_MLOPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([#;].*)?$")
# definition of section line:
# - then line MUST start with an opening square bracket - it is the first section marker
# - the section marker MUST be followed by at least one character - it is the section name
# - the section name MUST be followed by a closing square bracket - it is the second section marker
# - the second section marker MAY be followed by any amount of whitespace characters
# - the second section marker MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_SECTION_RE = re.compile(r"\[(.+)]\s*([#;].*)?$")
# definition of option line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the value MAY be of any length and character
# - the value MAY contain any amount of trailing whitespaces
# - the value MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_OPTION_RE = re.compile(r"^([^:=\s]+)\s?[:=]\s*([^=:].*)\s*([#;].*)?$")
# definition of multiline option line:
# - the line MUST start with a word - it is the option name
# - the option name MUST be followed by a colon or an equal sign - it is the separator
# - the separator MUST NOT be followed by a value
# - the separator MAY have any amount of leading or trailing whitespaces
# - the separator MUST NOT be directly followed by a colon or equal sign
# - the separator MAY be followed by a # or ; - it is the comment marker
# - the inline comment MAY be of any length and character
_MLOPTION_RE = re.compile(r"^([^:=\s]+)\s*[:=]\s*([#;].*)?$")
# definition of comment line:
# - the line MAY start with any amount of whitespace characters
# - the line MUST contain a # or ; - it is the comment marker
# - the comment marker MAY be followed by any amount of whitespace characters
# - the comment MAY be of any length and character
_COMMENT_RE = re.compile(r"^\s*([#;].*)?$")
# definition of empty line:
# - the line MUST contain only whitespace characters
_EMPTY_LINE_RE = re.compile(r"^\s*$")
BOOLEAN_STATES = {

View File

@@ -21,4 +21,8 @@ testcases = [
"serial",
"/dev/serial/by-id/<your-mcu-id>",
),
("parameter_temperature_(°C): 155", "parameter_temperature_(°C)", "155"),
("parameter_humidity_(%_RH): 45", "parameter_humidity_(%_RH)", "45"),
("parameter_spool_weight_(%): 10", "parameter_spool_weight_(%)", "10"),
("path: /dev/shm/drying_box.json", "path", "/dev/shm/drying_box.json"),
]

View File

@@ -14,7 +14,7 @@ def parser():
return SimpleConfigParser()
class TestLineParsing:
class TestSingleLineParsing:
@pytest.mark.parametrize("given, expected", [*case_parse_section])
def test_parse_section(self, parser, given, expected):
parser._parse_section(given)

View File

@@ -14,4 +14,14 @@ testcases = [
("", False),
("# that's a comment", False),
("; that's a comment", False),
("parameter_humidity_(%_RH):", True),
("parameter_spool_weight_(%):", True),
("parameter_temperature_(°C):", True),
("parameter_humidity_(%_RH): 18.123", False),
("parameter_spool_weight_(%): 150", False),
("parameter_temperature_(°C): 30,5", False),
("trusted_clients:", True),
("trusted_clients: 192.168.1.0/24", False),
("cors_domains:", True),
("cors_domains: http://*.lan", False),
]

View File

@@ -26,5 +26,6 @@ testcases = [
("description: homing!", True),
("description: inline macro :-)", True),
("path: %GCODES_DIR%", True),
("path: /dev/shm/drying_box.json", True),
("serial = /dev/serial/by-id/<your-mcu-id>", True),
]

View File

@@ -137,7 +137,7 @@ def get_selection_input(question: str, option_list: List | Dict, default=None) -
else:
raise ValueError("Invalid option_list type")
Logger.print_error(INVALID_CHOICE)
Logger.print_error("Invalid option! Please select a valid option.", False)
def format_question(question: str, default=None) -> str: