Compare commits

..

4 Commits

Author SHA1 Message Date
dw-0
1d06bf76f3 Merge pull request #511 from dw-0/develop
Merge develop into master
2024-09-01 19:02:48 +02:00
dw-0
e438081c35 fix: update SimpleConfigParser submodule (#510) 2024-09-01 18:51:25 +02:00
dw-0
9f50f6fdd7 fix: y and n are invalid selections in KlipperFlashOverviewMenu (#508)
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
2024-09-01 18:31:15 +02:00
dw-0
0ee0fa3325 feat: KIAUH v6 - full rewrite of KIAUH in Python (#428) 2024-08-31 19:16:52 +02:00
11 changed files with 151 additions and 63 deletions

View File

@@ -2,13 +2,54 @@
This document covers possible important changes to KIAUH. This document covers possible important changes to KIAUH.
### 2024-08-31 (v6.0.0-alpha.1)
Long time no see, but here we are again!
A lot has happened in the background, but now it is time to take it out into the wild.
#### KIAUH has now reached version 6! Well, at least in an alpha state...
The project has seen a complete rewrite of the script from scratch in Python.
It requires Python 3.8 or newer to run. Because this update is still in an alpha state, bugs may or will occur.
During startup, you will be asked if you want to start the new version 6 or the old version 5.
As long as version 6 is in a pre-release state, version 5 will still be available. If there are any critical issues
with the new version that were overlooked, you can always switch back to the old version.
In case you selected not to get asked about which version to start (option 3 or 4 in the startup dialog) and you want to
revert that decision, you will find a line called `version_to_launch=` within the `.kiauh.ini` file in your home directory.
Just delete that line, save the file and restart KIAUH. KIAUH will then ask you again which version you want to start.
Here is a list of the most important changes to KIAUH in regard to version 6:
- The majority of features available in KIAUH v5 are still available; they just got migrated from Bash to Python.
- It is now possible to add new/remove instances to/from existing multi-instance installations of Klipper and Moonraker
- KIAUH now has an Extension-System. This allows contributors to add new installers to KIAUH without having to modify the main script.
- You will now find some of the features that were previously available in the Installer-Menu in the Extensions-Menu.
- The current extensions are:
- G-Code Shell Command (previously found in the Advanced-Menu)
- Mainsail Theme Installer (previously found in the Advanced-Menu)
- Klipper-Backup (new in v6!)
- Moonraker Telegram Bot (previously found in the Installer-Menu)
- PrettyGCode for Klipper (previously found in the Installer-Menu)
- Obico for Klipper (previously found in the Installer-Menu)
- The following additional extensions are planned, but not yet available:
- Spoolman (available in v5 in the Installer-Menu)
- OctoApp (available in v5 in the Installer-Menu)
- KIAUH has its own config file now
- The file has some default values for the currently supported options
- There might be more options in the future
- It is located in KIAUH's root directory and is called `default.kiauh.cfg`
- DO NOT EDIT the default file directly, instead make a copy of it and call it `kiauh.cfg`
- Settings changed via the Advanced-Menu will be written to the `kiauh.cfg`
- Support for OctoPrint was removed
Feel free to give version 6 a try and report any bugs or issues you encounter! Every feedback is appreciated.
### 2023-06-17 ### 2023-06-17
KIAUH has now added support for installing Mobileraker's companion! KIAUH has now added support for installing Mobileraker's companion!
Mobileraker is a free and Open Source Android and iOS App for Klipper, utilizing the Moonraker API, allowing you Mobileraker is a free and Open Source Android and iOS App for Klipper, utilizing the Moonraker API, allowing you
to control your printer. Thank you to [Clon1998](https://github.com/Clon1998) for adding this feature! to control your printer. Thank you to [Clon1998](https://github.com/Clon1998) for adding this feature!
### 2023-02-03 ### 2023-02-03
The installer for MJPG-Streamer got replaced by crowsnest. It is an improved webcam service, utilizing ustreamer. The installer for MJPG-Streamer got replaced by crowsnest. It is an improved webcam service, utilizing ustreamer.
Please have a look here for additional info about crowsnest and how to configure it: https://github.com/mainsail-crew/crowsnest \ Please have a look here for additional info about crowsnest and how to configure it: https://github.com/mainsail-crew/crowsnest \
It's unsure if the previous MJPG-Streamer installer will be updated and make its way back into KIAUH. It's unsure if the previous MJPG-Streamer installer will be updated and make its way back into KIAUH.
A big thanks to [KwadFan](https://github.com/KwadFan) for writing the crowsnest implementation. A big thanks to [KwadFan](https://github.com/KwadFan) for writing the crowsnest implementation.
@@ -115,7 +156,7 @@ membership for example caused issues when installing mjpg-streamer while not usi
Other issues could occur when trying to flash an MCU on Debian or Ubuntu distributions where a user might not be part Other issues could occur when trying to flash an MCU on Debian or Ubuntu distributions where a user might not be part
of the dialout group by default. A check for the tty group is also done. The tty group is needed for setting of the dialout group by default. A check for the tty group is also done. The tty group is needed for setting
up a linux MCU (currently not yet supported by KIAUH). up a linux MCU (currently not yet supported by KIAUH).
* There is an issue when trying to install Mainsail or Fluidd on Ubuntu 21.10. Permissions on that distro seem to have seen a rework * There is an issue when trying to install Mainsail or Fluidd on Ubuntu 21.10. Permissions on that distro seem to have seen a rework
in comparison to 20.04 and users will be greeted with an "Error 403 - Permission denied" message after installing one of Klippers webinterfaces. in comparison to 20.04 and users will be greeted with an "Error 403 - Permission denied" message after installing one of Klippers webinterfaces.
I still have to figure out a viable solution for that. I still have to figure out a viable solution for that.

View File

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

View File

@@ -249,7 +249,7 @@ class KlipperSelectMcuIdMenu(BaseMenu):
self.flash_options = FlashOptions() self.flash_options = FlashOptions()
self.mcu_list = self.flash_options.mcu_list self.mcu_list = self.flash_options.mcu_list
self.input_label_txt = "Select MCU to flash" 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: def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
self.previous_menu = ( self.previous_menu = (
@@ -265,7 +265,7 @@ class KlipperSelectMcuIdMenu(BaseMenu):
def print_menu(self) -> None: def print_menu(self) -> None:
header = "!!! ATTENTION !!!" 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 color = COLOR_RED
count = 62 - len(color) - len(RESET_FORMAT) count = 62 - len(color) - len(RESET_FORMAT)
menu = textwrap.dedent( menu = textwrap.dedent(
@@ -277,15 +277,21 @@ class KlipperSelectMcuIdMenu(BaseMenu):
║ ONLY flash a firmware created for the respective MCU! ║ ║ ONLY flash a firmware created for the respective MCU! ║
║ ║ ║ ║
{header2:─^64} {header2:─^64}
║ ║
""" """
)[1:] )[1:]
for i, mcu in enumerate(self.mcu_list): for i, mcu in enumerate(self.mcu_list):
mcu = mcu.split("/")[-1] mcu = mcu.split("/")[-1]
menu += f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n" menu += f" {i}) {COLOR_CYAN}{mcu:<51}{RESET_FORMAT}\n"
menu += "╟───────────────────────────┬───────────────────────────╢"
print(menu, end="\n") menu += textwrap.dedent(
"""
║ ║
╟───────────────────────────────────────────────────────╢
"""
)[1:]
print(menu, end="")
def flash_mcu(self, **kwargs): def flash_mcu(self, **kwargs):
try: try:
@@ -343,8 +349,8 @@ class KlipperSelectSDFlashBoardMenu(BaseMenu):
for i, board in enumerate(self.available_boards): for i, board in enumerate(self.available_boards):
line = f" {i}) {board}" line = f" {i}) {board}"
menu += f"|{line:<55}|\n" menu += f"{line:<55}\n"
menu += "╟───────────────────────────────────────────────────────╢"
print(menu, end="") print(menu, end="")
def board_select(self, **kwargs): def board_select(self, **kwargs):
@@ -392,8 +398,8 @@ class KlipperFlashOverviewMenu(BaseMenu):
def set_options(self) -> None: def set_options(self) -> None:
self.options = { self.options = {
"Y": Option(self.execute_flash), "y": Option(self.execute_flash),
"N": Option(self.abort_process), "n": Option(self.abort_process),
} }
self.default_option = Option(self.execute_flash) self.default_option = Option(self.execute_flash)
@@ -406,7 +412,7 @@ class KlipperFlashOverviewMenu(BaseMenu):
method = self.flash_options.flash_method.value method = self.flash_options.flash_method.value
command = self.flash_options.flash_command.value command = self.flash_options.flash_command.value
conn_type = self.flash_options.connection_type.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 board = self.flash_options.selected_board
baudrate = self.flash_options.selected_baudrate baudrate = self.flash_options.selected_baudrate
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]" subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]"
@@ -420,26 +426,37 @@ class KlipperFlashOverviewMenu(BaseMenu):
║ sure everything is correct, start the process. If any ║ ║ sure everything is correct, start the process. If any ║
║ parameter needs to be changed, you can go back (B) ║ ║ parameter needs to be changed, you can go back (B) ║
║ step by step or abort and start from the beginning. ║ ║ step by step or abort and start from the beginning. ║
{subheader:-^64} {subheader:^64}
║ ║
""" """
)[1:] )[1:]
menu += f" ● MCU: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n" menu += textwrap.dedent(
menu += f" ● Connection: {COLOR_CYAN}{conn_type}{RESET_FORMAT}\n" f"""
menu += f" ● Flash method: {COLOR_CYAN}{method}{RESET_FORMAT}\n" ║ MCU: {COLOR_CYAN}{mcu:<48}{RESET_FORMAT}
menu += f" ● Flash command: {COLOR_CYAN}{command}{RESET_FORMAT}\n" ║ 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: if self.flash_options.flash_method is FlashMethod.SD_CARD:
menu += f" ● Board type: {COLOR_CYAN}{board}{RESET_FORMAT}\n" menu += textwrap.dedent(
menu += f" ● Baudrate: {COLOR_CYAN}{baudrate}{RESET_FORMAT}\n" f"""
║ Board type: {COLOR_CYAN}{board:<41}{RESET_FORMAT}
║ Baudrate: {COLOR_CYAN}{baudrate:<43}{RESET_FORMAT}
"""
)[1:]
menu += textwrap.dedent( menu += textwrap.dedent(
""" """
║ ║
╟───────────────────────────────────────────────────────╢ ╟───────────────────────────────────────────────────────╢
║ Y) Start flash process ║ ║ Y) Start flash process ║
║ N) Abort - Return to Advanced Menu ║ ║ N) Abort - Return to Advanced Menu ║
╟───────────────────────────────────────────────────────╢
""" """
) )[1:]
print(menu, end="") print(menu, end="")
def execute_flash(self, **kwargs): 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 :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_index: str = ""
opt_data: Any = None opt_data: Any = None

View File

@@ -25,6 +25,7 @@ from core.constants import (
) )
from core.logger import Logger from core.logger import Logger
from core.menus import FooterType, Option from core.menus import FooterType, Option
from utils.input_utils import get_selection_input
def clear() -> None: def clear() -> None:
@@ -141,7 +142,7 @@ class BaseMenu(metaclass=PostInitCaller):
def __go_to_help(self, **kwargs) -> None: def __go_to_help(self, **kwargs) -> None:
if self.help_menu is None: if self.help_menu is None:
return return
self.help_menu(previous_menu=self).run() self.help_menu(previous_menu=self.__class__).run()
def __exit(self, **kwargs) -> None: def __exit(self, **kwargs) -> None:
Logger.print_ok("###### Happy printing!", False) Logger.print_ok("###### Happy printing!", False)
@@ -177,46 +178,20 @@ class BaseMenu(metaclass=PostInitCaller):
self.print_menu() self.print_menu()
self.print_footer() 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: def run(self) -> None:
"""Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends."""
try: try:
self.display_menu() self.display_menu()
option = self.handle_user_input() option = get_selection_input(self.input_label_txt, self.options)
option.method(opt_index=option.opt_index, opt_data=option.opt_data) selected_option: Option = self.options.get(option)
selected_option.method(
opt_index=selected_option.opt_index,
opt_data=selected_option.opt_data,
)
self.run() self.run()
except Exception as e: except Exception as e:
Logger.print_error( Logger.print_error(
f"An unexpected error occured:\n{e}\n{traceback.format_exc()}" f"An unexpected error occured:\n{e}\n{traceback.format_exc()}"

View File

@@ -85,10 +85,46 @@ class DuplicateOptionError(Exception):
class SimpleConfigParser: class SimpleConfigParser:
"""A customized config parser targeted at handling Klipper style config files""" """A customized config parser targeted at handling Klipper style config files"""
_SECTION_RE = re.compile(r"\s*\[(\w+\s?.+)]\s*([#;].*)?$") # definition of section line:
_OPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([^=:].*)\s*([#;].*)?$") # - then line MUST start with an opening square bracket - it is the first section marker
_MLOPTION_RE = re.compile(r"^\s*(\w+)\s*[:=]\s*([#;].*)?$") # - 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*([#;].*)?$") _COMMENT_RE = re.compile(r"^\s*([#;].*)?$")
# definition of empty line:
# - the line MUST contain only whitespace characters
_EMPTY_LINE_RE = re.compile(r"^\s*$") _EMPTY_LINE_RE = re.compile(r"^\s*$")
BOOLEAN_STATES = { BOOLEAN_STATES = {

View File

@@ -21,4 +21,8 @@ testcases = [
"serial", "serial",
"/dev/serial/by-id/<your-mcu-id>", "/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() return SimpleConfigParser()
class TestLineParsing: class TestSingleLineParsing:
@pytest.mark.parametrize("given, expected", [*case_parse_section]) @pytest.mark.parametrize("given, expected", [*case_parse_section])
def test_parse_section(self, parser, given, expected): def test_parse_section(self, parser, given, expected):
parser._parse_section(given) parser._parse_section(given)

View File

@@ -14,4 +14,14 @@ testcases = [
("", False), ("", False),
("# that's a comment", False), ("# that's a comment", 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: homing!", True),
("description: inline macro :-)", True), ("description: inline macro :-)", True),
("path: %GCODES_DIR%", True), ("path: %GCODES_DIR%", True),
("path: /dev/shm/drying_box.json", True),
("serial = /dev/serial/by-id/<your-mcu-id>", 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: else:
raise ValueError("Invalid option_list type") 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: def format_question(question: str, default=None) -> str: