mirror of
https://github.com/dw-0/kiauh.git
synced 2025-12-14 19:14:27 +05:00
Compare commits
19 Commits
v6.0.0-alp
...
v6.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
425d86a12f | ||
|
|
ff6162d799 | ||
|
|
674c174224 | ||
|
|
a368331693 | ||
|
|
406b64d1e5 | ||
|
|
1b5691f2f5 | ||
|
|
e7eae5a0d1 | ||
|
|
dc561a562c | ||
|
|
55cfe124b2 | ||
|
|
43d6598be6 | ||
|
|
dc026a7a2b | ||
|
|
ac54d04b40 | ||
|
|
c19364360c | ||
|
|
2e6c66e524 | ||
|
|
cd8003add9 | ||
|
|
1f75395063 | ||
|
|
6e1bffa975 | ||
|
|
a8a73249a5 | ||
|
|
4138c71920 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
||||
.idea
|
||||
.vscode
|
||||
.pytest_cache
|
||||
.jupyter
|
||||
*.ipynb
|
||||
*.ipynb_checkpoints
|
||||
*.tmp
|
||||
__pycache__
|
||||
.kiauh-env
|
||||
*.code-workspace
|
||||
|
||||
@@ -14,5 +14,5 @@ port: 80
|
||||
unstable_releases: False
|
||||
|
||||
[fluidd]
|
||||
port: 81
|
||||
port: 80
|
||||
unstable_releases: False
|
||||
|
||||
@@ -6,8 +6,16 @@
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, Popen, check_output, run
|
||||
import re
|
||||
from subprocess import (
|
||||
DEVNULL,
|
||||
PIPE,
|
||||
STDOUT,
|
||||
CalledProcessError,
|
||||
Popen,
|
||||
check_output,
|
||||
run,
|
||||
)
|
||||
from typing import List
|
||||
|
||||
from components.klipper import KLIPPER_DIR
|
||||
@@ -32,16 +40,18 @@ def find_firmware_file() -> bool:
|
||||
f3 = "klipper.bin"
|
||||
f4 = "klipper.uf2"
|
||||
fw_file_exists: bool = (
|
||||
target.joinpath(f1).exists() and target.joinpath(f2).exists()
|
||||
) or target.joinpath(f3).exists() or target.joinpath(f4).exists()
|
||||
(target.joinpath(f1).exists() and target.joinpath(f2).exists())
|
||||
or target.joinpath(f3).exists()
|
||||
or target.joinpath(f4).exists()
|
||||
)
|
||||
|
||||
return target_exists and fw_file_exists
|
||||
|
||||
|
||||
def find_usb_device_by_id() -> List[str]:
|
||||
try:
|
||||
command = "find /dev/serial/by-id/* 2>/dev/null"
|
||||
output = check_output(command, shell=True, text=True)
|
||||
command = "find /dev/serial/by-id/*"
|
||||
output = check_output(command, shell=True, text=True, stderr=DEVNULL)
|
||||
return output.splitlines()
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error("Unable to find a USB device!")
|
||||
@@ -51,9 +61,14 @@ def find_usb_device_by_id() -> List[str]:
|
||||
|
||||
def find_uart_device() -> List[str]:
|
||||
try:
|
||||
command = '"find /dev -maxdepth 1 -regextype posix-extended -regex "^\/dev\/tty(AMA0|S0)$" 2>/dev/null"'
|
||||
output = check_output(command, shell=True, text=True)
|
||||
return output.splitlines()
|
||||
cmd = "find /dev -maxdepth 1"
|
||||
output = check_output(cmd, shell=True, text=True, stderr=DEVNULL)
|
||||
device_list = []
|
||||
if output:
|
||||
pattern = r"^/dev/tty(AMA0|S0)$"
|
||||
devices = output.splitlines()
|
||||
device_list = [d for d in devices if re.search(pattern, d)]
|
||||
return device_list
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error("Unable to find a UART device!")
|
||||
Logger.print_error(e, prefix=False)
|
||||
@@ -62,15 +77,34 @@ def find_uart_device() -> List[str]:
|
||||
|
||||
def find_usb_dfu_device() -> List[str]:
|
||||
try:
|
||||
command = '"lsusb | grep "DFU" | cut -d " " -f 6 2>/dev/null"'
|
||||
output = check_output(command, shell=True, text=True)
|
||||
return output.splitlines()
|
||||
output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL)
|
||||
device_list = []
|
||||
if output:
|
||||
devices = output.splitlines()
|
||||
device_list = [d.split(" ")[5] for d in devices if "DFU" in d]
|
||||
return device_list
|
||||
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error("Unable to find a USB DFU device!")
|
||||
Logger.print_error(e, prefix=False)
|
||||
return []
|
||||
|
||||
|
||||
def find_usb_rp2_boot_device() -> List[str]:
|
||||
try:
|
||||
output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL)
|
||||
device_list = []
|
||||
if output:
|
||||
devices = output.splitlines()
|
||||
device_list = [d.split(" ")[5] for d in devices if "RP2 Boot" in d]
|
||||
return device_list
|
||||
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error("Unable to find a USB RP2 Boot device!")
|
||||
Logger.print_error(e, prefix=False)
|
||||
return []
|
||||
|
||||
|
||||
def get_sd_flash_board_list() -> List[str]:
|
||||
if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists():
|
||||
return []
|
||||
|
||||
@@ -26,6 +26,7 @@ class FlashCommand(Enum):
|
||||
class ConnectionType(Enum):
|
||||
USB = "USB"
|
||||
USB_DFU = "USB (DFU)"
|
||||
USB_RP2040 = "USB (RP2040)"
|
||||
UART = "UART"
|
||||
|
||||
|
||||
|
||||
@@ -143,6 +143,8 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
|
||||
count = 62 - len(color) - len(RESET_FORMAT)
|
||||
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}"
|
||||
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
|
||||
subheader3 = f"{COLOR_CYAN}USB DFU:{RESET_FORMAT}"
|
||||
subheader4 = f"{COLOR_CYAN}USB RP2040 Boot:{RESET_FORMAT}"
|
||||
menu = textwrap.dedent(
|
||||
f"""
|
||||
╔═══════════════════════════════════════════════════════╗
|
||||
@@ -164,6 +166,19 @@ class KlipperMcuConnectionHelpMenu(BaseMenu):
|
||||
║ port your controller board is connected to when using ║
|
||||
║ this connection method. ║
|
||||
║ ║
|
||||
║ {subheader3:<62} ║
|
||||
║ Selecting USB DFU as the connection method will scan ║
|
||||
║ the USB ports for connected controller boards in ║
|
||||
║ STM32 DFU mode, which is usually done by holding down ║
|
||||
║ the BOOT button or setting a special jumper on the ║
|
||||
║ board before powering up. ║
|
||||
║ ║
|
||||
║ {subheader4:<62} ║
|
||||
║ Selecting USB RP2 Boot as the connection method will ║
|
||||
║ scan the USB ports for connected RP2040 controller ║
|
||||
║ boards in Boot mode, which is usually done by holding ║
|
||||
║ down the BOOT button before powering up. ║
|
||||
║ ║
|
||||
╟───────────────────────────────────────────────────────╢
|
||||
"""
|
||||
)[1:]
|
||||
|
||||
@@ -17,6 +17,7 @@ from components.klipper_firmware.firmware_utils import (
|
||||
find_uart_device,
|
||||
find_usb_device_by_id,
|
||||
find_usb_dfu_device,
|
||||
find_usb_rp2_boot_device,
|
||||
get_sd_flash_board_list,
|
||||
start_flash_process,
|
||||
)
|
||||
@@ -177,6 +178,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
|
||||
"1": Option(method=self.select_usb),
|
||||
"2": Option(method=self.select_dfu),
|
||||
"3": Option(method=self.select_usb_dfu),
|
||||
"4": Option(method=self.select_usb_rp2040),
|
||||
}
|
||||
|
||||
def print_menu(self) -> None:
|
||||
@@ -193,6 +195,7 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
|
||||
║ 1) USB ║
|
||||
║ 2) UART ║
|
||||
║ 3) USB (DFU mode) ║
|
||||
║ 4) USB (RP2040 mode) ║
|
||||
╟───────────────────────────┬───────────────────────────╢
|
||||
"""
|
||||
)[1:]
|
||||
@@ -210,6 +213,10 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
|
||||
self.flash_options.connection_type = ConnectionType.USB_DFU
|
||||
self.get_mcu_list()
|
||||
|
||||
def select_usb_rp2040(self, **kwargs):
|
||||
self.flash_options.connection_type = ConnectionType.USB_RP2040
|
||||
self.get_mcu_list()
|
||||
|
||||
def get_mcu_list(self, **kwargs):
|
||||
conn_type = self.flash_options.connection_type
|
||||
|
||||
@@ -222,6 +229,9 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
|
||||
elif conn_type is ConnectionType.USB_DFU:
|
||||
Logger.print_status("Identifying MCU connected via USB in DFU mode ...")
|
||||
self.flash_options.mcu_list = find_usb_dfu_device()
|
||||
elif conn_type is ConnectionType.USB_RP2040:
|
||||
Logger.print_status("Identifying MCU connected via USB in RP2 Boot mode ...")
|
||||
self.flash_options.mcu_list = find_usb_rp2_boot_device()
|
||||
|
||||
if len(self.flash_options.mcu_list) < 1:
|
||||
Logger.print_warn("No MCUs found!")
|
||||
|
||||
@@ -13,15 +13,15 @@ from components.webui_client.base_data import BaseWebClient
|
||||
from core.logger import DialogType, Logger
|
||||
|
||||
|
||||
def print_moonraker_not_found_dialog() -> None:
|
||||
def print_moonraker_not_found_dialog(name: str) -> None:
|
||||
Logger.print_dialog(
|
||||
DialogType.WARNING,
|
||||
[
|
||||
"No local Moonraker installation was found!",
|
||||
"\n\n",
|
||||
"It is possible to install Mainsail without a local Moonraker installation. "
|
||||
f"It is possible to install {name} without a local Moonraker installation. "
|
||||
"If you continue, you need to make sure, that Moonraker is installed on "
|
||||
"another machine in your network. Otherwise Mainsail will NOT work "
|
||||
f"another machine in your network. Otherwise {name} will NOT work "
|
||||
"correctly.",
|
||||
],
|
||||
)
|
||||
@@ -40,20 +40,25 @@ def print_client_already_installed_dialog(name: str) -> None:
|
||||
def print_client_port_select_dialog(
|
||||
name: str, port: int, ports_in_use: List[int]
|
||||
) -> None:
|
||||
Logger.print_dialog(
|
||||
DialogType.CUSTOM,
|
||||
[
|
||||
f"Please select the port, {name} should be served on. If your are unsure "
|
||||
f"what to select, hit Enter to apply the suggested value of: {port}",
|
||||
"\n\n",
|
||||
f"In case you need {name} to be served on a specific port, you can set it "
|
||||
f"now. Make sure that the port is not already used by another application "
|
||||
f"on your system!",
|
||||
"\n\n",
|
||||
"The following ports were found to be in use already:",
|
||||
*[f"● {port}" for port in ports_in_use],
|
||||
],
|
||||
)
|
||||
dialog_content: List[str] = [
|
||||
f"Please select the port, {name} should be served on. If your are unsure "
|
||||
f"what to select, hit Enter to apply the suggested value of: {port}",
|
||||
"\n\n",
|
||||
f"In case you need {name} to be served on a specific port, you can set it "
|
||||
f"now. Make sure that the port is not already used by another application "
|
||||
f"on your system!",
|
||||
]
|
||||
|
||||
if ports_in_use:
|
||||
dialog_content.extend(
|
||||
[
|
||||
"\n\n",
|
||||
"The following ports were found to be in use already:",
|
||||
*[f"● {port}" for port in ports_in_use],
|
||||
]
|
||||
)
|
||||
|
||||
Logger.print_dialog(DialogType.CUSTOM, dialog_content)
|
||||
|
||||
|
||||
def print_install_client_config_dialog(client: BaseWebClient) -> None:
|
||||
|
||||
@@ -23,7 +23,6 @@ from components.webui_client.client_config.client_config_setup import (
|
||||
install_client_config,
|
||||
)
|
||||
from components.webui_client.client_dialogs import (
|
||||
print_client_port_select_dialog,
|
||||
print_install_client_config_dialog,
|
||||
print_moonraker_not_found_dialog,
|
||||
)
|
||||
@@ -33,18 +32,15 @@ from components.webui_client.client_utils import (
|
||||
create_nginx_cfg,
|
||||
detect_client_cfg_conflict,
|
||||
enable_mainsail_remotemode,
|
||||
get_next_free_port,
|
||||
is_valid_port,
|
||||
read_ports_from_nginx_configs,
|
||||
get_client_port_selection,
|
||||
symlink_webui_nginx_log,
|
||||
)
|
||||
from core.instance_manager.instance_manager import InstanceManager
|
||||
from core.logger import Logger
|
||||
from core.settings.kiauh_settings import KiauhSettings
|
||||
from utils.common import check_install_dependencies
|
||||
from utils.config_utils import add_config_section
|
||||
from utils.fs_utils import unzip
|
||||
from utils.input_utils import get_confirm, get_number_input
|
||||
from utils.input_utils import get_confirm
|
||||
from utils.instance_utils import get_instances
|
||||
from utils.sys_utils import (
|
||||
cmd_sysctl_service,
|
||||
@@ -67,7 +63,7 @@ def install_client(client: BaseWebClient) -> None:
|
||||
|
||||
enable_remotemode = False
|
||||
if not mr_instances:
|
||||
print_moonraker_not_found_dialog()
|
||||
print_moonraker_not_found_dialog(client.display_name)
|
||||
if not get_confirm(f"Continue {client.display_name} installation?"):
|
||||
return
|
||||
|
||||
@@ -92,21 +88,7 @@ def install_client(client: BaseWebClient) -> None:
|
||||
question = f"Download the recommended {client_config.display_name}?"
|
||||
install_client_cfg = get_confirm(question, allow_go_back=False)
|
||||
|
||||
settings = KiauhSettings()
|
||||
port: int = settings.get(client.name, "port")
|
||||
ports_in_use: List[int] = read_ports_from_nginx_configs()
|
||||
|
||||
# check if configured port is a valid number and not in use already
|
||||
valid_port = is_valid_port(port, ports_in_use)
|
||||
while not valid_port:
|
||||
next_port = get_next_free_port(ports_in_use)
|
||||
print_client_port_select_dialog(client.display_name, next_port, ports_in_use)
|
||||
port = get_number_input(
|
||||
f"Configure {client.display_name} for port",
|
||||
min_count=int(next_port),
|
||||
default=next_port,
|
||||
)
|
||||
valid_port = is_valid_port(port, ports_in_use)
|
||||
port: int = get_client_port_selection(client)
|
||||
|
||||
check_install_dependencies({"nginx"})
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from components.webui_client.base_data import (
|
||||
BaseWebClient,
|
||||
WebClientType,
|
||||
)
|
||||
from components.webui_client.client_dialogs import print_client_port_select_dialog
|
||||
from components.webui_client.fluidd_data import FluiddData
|
||||
from components.webui_client.mainsail_data import MainsailData
|
||||
from core.backup_manager.backup_manager import BackupManager
|
||||
@@ -33,7 +34,7 @@ from core.constants import (
|
||||
RESET_FORMAT,
|
||||
)
|
||||
from core.logger import Logger
|
||||
from core.settings.kiauh_settings import KiauhSettings
|
||||
from core.settings.kiauh_settings import KiauhSettings, WebUiSettings
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
@@ -44,6 +45,7 @@ from utils.git_utils import (
|
||||
get_latest_remote_tag,
|
||||
get_latest_unstable_tag,
|
||||
)
|
||||
from utils.input_utils import get_number_input
|
||||
from utils.instance_utils import get_instances
|
||||
|
||||
|
||||
@@ -353,17 +355,44 @@ def read_ports_from_nginx_configs() -> List[int]:
|
||||
lines = cfg.readlines()
|
||||
|
||||
for line in lines:
|
||||
line = line.replace("default_server", "")
|
||||
line = re.sub(r"[;:\[\]]", "", line.strip())
|
||||
if line.startswith("listen") and line.split()[-1] not in port_list:
|
||||
port_list.append(line.split()[-1])
|
||||
line = re.sub(
|
||||
r"default_server|http://|https://|[;\[\]]",
|
||||
"",
|
||||
line.strip(),
|
||||
)
|
||||
if line.startswith("listen"):
|
||||
if ":" not in line:
|
||||
port_list.append(line.split()[-1])
|
||||
else:
|
||||
port_list.append(line.split(":")[-1])
|
||||
|
||||
ports_to_ints_list = [int(port) for port in port_list]
|
||||
return sorted(ports_to_ints_list, key=lambda x: int(x))
|
||||
|
||||
|
||||
def is_valid_port(port: int, ports_in_use: List[int]) -> bool:
|
||||
return port not in ports_in_use
|
||||
def get_client_port_selection(client: BaseWebClient) -> int:
|
||||
settings = KiauhSettings()
|
||||
default_port: int = int(settings.get(client.name, "port"))
|
||||
|
||||
ports_in_use: List[int] = read_ports_from_nginx_configs()
|
||||
next_free_port: int = get_next_free_port(ports_in_use)
|
||||
|
||||
port: int = next_free_port if default_port in ports_in_use else default_port
|
||||
|
||||
print_client_port_select_dialog(client.display_name, port, ports_in_use)
|
||||
|
||||
while True:
|
||||
question = f"Configure {client.display_name} for port"
|
||||
port_input = get_number_input(question, min_count=80, default=port)
|
||||
|
||||
if port_input not in ports_in_use:
|
||||
client_settings: WebUiSettings = settings[client.name]
|
||||
client_settings.port = port_input
|
||||
settings.save()
|
||||
|
||||
return port_input
|
||||
|
||||
Logger.print_error("This port is already in use. Please select another one.")
|
||||
|
||||
|
||||
def get_next_free_port(ports_in_use: List[int]) -> int:
|
||||
|
||||
@@ -33,7 +33,7 @@ CURRENT_USER = pwd.getpwuid(os.getuid())[0]
|
||||
|
||||
# dirs
|
||||
SYSTEMD = Path("/etc/systemd/system")
|
||||
PRINTER_CFG_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-cfg-backups")
|
||||
PRINTER_DATA_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("printer-data-backups")
|
||||
NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available")
|
||||
NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled")
|
||||
NGINX_CONFD = Path("/etc/nginx/conf.d")
|
||||
|
||||
@@ -192,11 +192,24 @@ class SimpleConfigParser:
|
||||
self.config[section] = {"_raw": f"[{section}]\n"}
|
||||
|
||||
def _check_set_section_spacing(self):
|
||||
prev_section = self.get_sections()[-1]
|
||||
prev_section_content = self.config[prev_section]
|
||||
last_item = list(prev_section_content.keys())[-1]
|
||||
if last_item.startswith("#_") and last_item.keys()[-1] != "\n":
|
||||
prev_section_content[last_item].append("\n")
|
||||
prev_section_name: str = self.get_sections()[-1]
|
||||
prev_section_content: Dict = self.config[prev_section_name]
|
||||
last_option_name: str = list(prev_section_content.keys())[-1]
|
||||
|
||||
if last_option_name.startswith("#_"):
|
||||
last_elem_value: str = prev_section_content[last_option_name][-1]
|
||||
|
||||
# if the last section is a collector, we first check if the last element
|
||||
# in the collector ends with a newline. if it does not, we append a newline.
|
||||
# this can happen if the config file does not end with a newline.
|
||||
if not last_elem_value.endswith("\n"):
|
||||
prev_section_content[last_option_name][-1] = f"{last_elem_value}\n"
|
||||
|
||||
# if the last item in a collector is not a newline, we append a newline, so
|
||||
# that the new section is seperated from the options of the previous section
|
||||
# by a newline
|
||||
if last_elem_value != "\n":
|
||||
prev_section_content[last_option_name].append("\n")
|
||||
else:
|
||||
prev_section_content[self._generate_rand_id()] = ["\n"]
|
||||
|
||||
@@ -298,7 +311,7 @@ class SimpleConfigParser:
|
||||
"""Return the value of the given option in the given section as a converted value"""
|
||||
try:
|
||||
return conv(self.getval(section, option, fallback))
|
||||
except ValueError as e:
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
if fallback is not _UNSET:
|
||||
return fallback
|
||||
raise ValueError(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# a comment at the very top
|
||||
# should be treated as the file header
|
||||
|
||||
# up to the first section, including all blank lines
|
||||
|
||||
[section_1]
|
||||
option_1: value_1
|
||||
option_1_1: True # this is a boolean
|
||||
option_1_2: 5 ; this is an integer
|
||||
option_1_3: 1.123 #;this is a float
|
||||
|
||||
[section_2] ; comment
|
||||
option_2: value_2
|
||||
|
||||
; comment
|
||||
|
||||
[section_3]
|
||||
option_3: value_3 # comment
|
||||
|
||||
[section_4]
|
||||
# comment
|
||||
option_4: value_4
|
||||
|
||||
[section number 5]
|
||||
#option_5: value_5
|
||||
option_5 = this.is.value-5
|
||||
multi_option:
|
||||
# these are multi-line values
|
||||
value_5_1
|
||||
value_5_2 ; here is a comment
|
||||
value_5_3
|
||||
option_5_1: value_5_1
|
||||
# config ending with a comment
|
||||
@@ -0,0 +1,94 @@
|
||||
# a comment at the very top
|
||||
# should be treated as the file header
|
||||
|
||||
# up to the first section, including all blank lines
|
||||
|
||||
[section_1]
|
||||
option_1: value_1
|
||||
option_1_1: True # this is a boolean
|
||||
option_1_2: 5 ; this is an integer
|
||||
option_1_3: 1.123 #;this is a float
|
||||
|
||||
[section_2] ; comment
|
||||
option_2: value_2
|
||||
|
||||
; comment
|
||||
|
||||
[section_3]
|
||||
option_3: value_3 # comment
|
||||
|
||||
[section_4]
|
||||
# comment
|
||||
option_4: value_4
|
||||
|
||||
[section number 5]
|
||||
#option_5: value_5
|
||||
option_5 = this.is.value-5
|
||||
multi_option:
|
||||
# these are multi-line values
|
||||
value_5_1
|
||||
value_5_2 ; here is a comment
|
||||
value_5_3
|
||||
option_5_1: value_5_1
|
||||
|
||||
[gcode_macro M117]
|
||||
rename_existing: M117.1
|
||||
gcode:
|
||||
{% if rawparams %}
|
||||
{% set escaped_msg = rawparams.split(';', 1)[0].split('\x23', 1)[0]|replace('"', '\\"') %}
|
||||
SET_DISPLAY_TEXT MSG="{escaped_msg}"
|
||||
RESPOND TYPE=command MSG="{escaped_msg}"
|
||||
{% else %}
|
||||
SET_DISPLAY_TEXT
|
||||
{% endif %}
|
||||
|
||||
# SDCard 'looping' (aka Marlin M808 commands) support
|
||||
#
|
||||
# Support SDCard looping
|
||||
[sdcard_loop]
|
||||
[gcode_macro M486]
|
||||
gcode:
|
||||
# Parameters known to M486 are as follows:
|
||||
# [C<flag>] Cancel the current object
|
||||
# [P<index>] Cancel the object with the given index
|
||||
# [S<index>] Set the index of the current object.
|
||||
# If the object with the given index has been canceled, this will cause
|
||||
# the firmware to skip to the next object. The value -1 is used to
|
||||
# indicate something that isn’t an object and shouldn’t be skipped.
|
||||
# [T<count>] Reset the state and set the number of objects
|
||||
# [U<index>] Un-cancel the object with the given index. This command will be
|
||||
# ignored if the object has already been skipped
|
||||
|
||||
{% if 'exclude_object' not in printer %}
|
||||
{action_raise_error("[exclude_object] is not enabled")}
|
||||
{% endif %}
|
||||
|
||||
{% if 'T' in params %}
|
||||
EXCLUDE_OBJECT RESET=1
|
||||
|
||||
{% for i in range(params.T | int) %}
|
||||
EXCLUDE_OBJECT_DEFINE NAME={i}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if 'C' in params %}
|
||||
EXCLUDE_OBJECT CURRENT=1
|
||||
{% endif %}
|
||||
|
||||
{% if 'P' in params %}
|
||||
EXCLUDE_OBJECT NAME={params.P}
|
||||
{% endif %}
|
||||
|
||||
{% if 'S' in params %}
|
||||
{% if params.S == '-1' %}
|
||||
{% if printer.exclude_object.current_object %}
|
||||
EXCLUDE_OBJECT_END NAME={printer.exclude_object.current_object}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
EXCLUDE_OBJECT_START NAME={params.S}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if 'U' in params %}
|
||||
EXCLUDE_OBJECT RESET=1 NAME={params.U}
|
||||
{% endif %}
|
||||
@@ -32,7 +32,7 @@ def test_section_parsing(parser):
|
||||
parser.config.keys()
|
||||
), f"Expected keys: {expected_keys}, got: {parser.config.keys()}"
|
||||
assert parser.in_option_block is False
|
||||
assert parser.current_section == "section number 5"
|
||||
assert parser.current_section == parser.get_sections()[-1]
|
||||
assert parser.config["section_2"]["_raw"] == "[section_2] ; comment"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# https://github.com/dw-0/simple-config-parser #
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from tests.utils import load_testdata_from_file
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
|
||||
CONFIG_FILES = ["test_config_1.cfg", "test_config_2.cfg", "test_config_3.cfg"]
|
||||
|
||||
|
||||
@pytest.fixture(params=CONFIG_FILES)
|
||||
def parser(request):
|
||||
parser = SimpleConfigParser()
|
||||
file_path = BASE_DIR.joinpath(request.param)
|
||||
for line in load_testdata_from_file(file_path):
|
||||
parser._parse_line(line) # noqa
|
||||
|
||||
return parser
|
||||
@@ -5,28 +5,13 @@
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.simple_config_parser.simple_config_parser import (
|
||||
NoOptionError,
|
||||
NoSectionError,
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from tests.utils import load_testdata_from_file
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
|
||||
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser():
|
||||
parser = SimpleConfigParser()
|
||||
for line in load_testdata_from_file(TEST_DATA_PATH):
|
||||
parser._parse_line(line) # noqa
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def test_get_options(parser):
|
||||
@@ -72,6 +57,7 @@ def test_getval(parser):
|
||||
|
||||
def test_getval_fallback(parser):
|
||||
assert parser.getval("section_1", "option_128", "fallback") == "fallback"
|
||||
assert parser.getval("section_1", "option_128", None) is None
|
||||
|
||||
|
||||
def test_getval_exceptions(parser):
|
||||
@@ -104,6 +90,7 @@ def test_getint_from_boolean(parser):
|
||||
|
||||
def test_getint_fallback(parser):
|
||||
assert parser.getint("section_1", "option_128", 128) == 128
|
||||
assert parser.getint("section_1", "option_128", None) is None
|
||||
|
||||
|
||||
def test_getboolean(parser):
|
||||
@@ -130,6 +117,7 @@ def test_getboolean_from_float(parser):
|
||||
def test_getboolean_fallback(parser):
|
||||
assert parser.getboolean("section_1", "option_128", True) is True
|
||||
assert parser.getboolean("section_1", "option_128", False) is False
|
||||
assert parser.getboolean("section_1", "option_128", None) is None
|
||||
|
||||
|
||||
def test_getfloat(parser):
|
||||
@@ -154,6 +142,7 @@ def test_getfloat_from_boolean(parser):
|
||||
|
||||
def test_getfloat_fallback(parser):
|
||||
assert parser.getfloat("section_1", "option_128", 1.234) == 1.234
|
||||
assert parser.getfloat("section_1", "option_128", None) is None
|
||||
|
||||
|
||||
def test_set_existing_option(parser):
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
# ======================================================================= #
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
@@ -17,12 +15,8 @@ BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
|
||||
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser():
|
||||
return SimpleConfigParser()
|
||||
|
||||
|
||||
def test_read_file(parser):
|
||||
def test_read_file():
|
||||
parser = SimpleConfigParser()
|
||||
parser.read_file(TEST_DATA_PATH)
|
||||
assert parser.config is not None
|
||||
assert parser.config.keys() is not None
|
||||
|
||||
@@ -5,27 +5,12 @@
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.simple_config_parser.simple_config_parser import (
|
||||
DuplicateSectionError,
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from tests.utils import load_testdata_from_file
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent.joinpath("assets")
|
||||
TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser():
|
||||
parser = SimpleConfigParser()
|
||||
for line in load_testdata_from_file(TEST_DATA_PATH):
|
||||
parser._parse_line(line) # noqa
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def test_get_sections(parser):
|
||||
|
||||
@@ -25,50 +25,65 @@ def parser():
|
||||
return parser
|
||||
|
||||
|
||||
def test_get_conv(parser):
|
||||
# Test conversion to int
|
||||
def test_get_int_conv(parser):
|
||||
should_be_int = parser._get_conv("section_1", "option_1_2", int)
|
||||
assert isinstance(should_be_int, int)
|
||||
|
||||
# Test conversion to float
|
||||
|
||||
def test_get_float_conv(parser):
|
||||
should_be_float = parser._get_conv("section_1", "option_1_3", float)
|
||||
assert isinstance(should_be_float, float)
|
||||
|
||||
# Test conversion to boolean
|
||||
|
||||
def test_get_bool_conv(parser):
|
||||
should_be_bool = parser._get_conv(
|
||||
"section_1", "option_1_1", parser._convert_to_boolean
|
||||
)
|
||||
assert isinstance(should_be_bool, bool)
|
||||
|
||||
# Test fallback for int
|
||||
|
||||
def test_get_int_conv_fallback(parser):
|
||||
should_be_fallback_int = parser._get_conv(
|
||||
"section_1", "option_128", int, fallback=128
|
||||
)
|
||||
assert isinstance(should_be_fallback_int, int)
|
||||
assert should_be_fallback_int == 128
|
||||
assert parser._get_conv("section_1", "option_128", int, None) is None
|
||||
|
||||
# Test fallback for float
|
||||
|
||||
def test_get_float_conv_fallback(parser):
|
||||
should_be_fallback_float = parser._get_conv(
|
||||
"section_1", "option_128", float, fallback=1.234
|
||||
)
|
||||
assert isinstance(should_be_fallback_float, float)
|
||||
assert should_be_fallback_float == 1.234
|
||||
|
||||
# Test fallback for boolean
|
||||
assert parser._get_conv("section_1", "option_128", float, None) is None
|
||||
|
||||
|
||||
def test_get_bool_conv_fallback(parser):
|
||||
should_be_fallback_bool = parser._get_conv(
|
||||
"section_1", "option_128", parser._convert_to_boolean, fallback=True
|
||||
)
|
||||
assert isinstance(should_be_fallback_bool, bool)
|
||||
assert should_be_fallback_bool is True
|
||||
|
||||
# Test ValueError exception for invalid int conversion
|
||||
assert (
|
||||
parser._get_conv("section_1", "option_128", parser._convert_to_boolean, None)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_get_int_conv_exception(parser):
|
||||
with pytest.raises(ValueError):
|
||||
parser._get_conv("section_1", "option_1", int)
|
||||
|
||||
# Test ValueError exception for invalid float conversion
|
||||
|
||||
def test_get_float_conv_exception(parser):
|
||||
with pytest.raises(ValueError):
|
||||
parser._get_conv("section_1", "option_1", float)
|
||||
|
||||
# Test ValueError exception for invalid boolean conversion
|
||||
|
||||
def test_get_bool_conv_exception(parser):
|
||||
with pytest.raises(ValueError):
|
||||
parser._get_conv("section_1", "option_1", parser._convert_to_boolean)
|
||||
|
||||
@@ -58,12 +58,16 @@ class ExtensionsMenu(BaseMenu):
|
||||
module_path = f"kiauh.extensions.{ext.name}.{module_name}"
|
||||
|
||||
# get the class name of the extension
|
||||
ext_class: Type[BaseExtension] = inspect.getmembers(
|
||||
importlib.import_module(module_path),
|
||||
predicate=lambda o: inspect.isclass(o)
|
||||
and issubclass(o, BaseExtension)
|
||||
and o != BaseExtension,
|
||||
)[0][1]
|
||||
module = importlib.import_module(module_path)
|
||||
|
||||
def predicate(o):
|
||||
return (
|
||||
inspect.isclass(o)
|
||||
and issubclass(o, BaseExtension)
|
||||
and o != BaseExtension
|
||||
)
|
||||
|
||||
ext_class: type = inspect.getmembers(module, predicate)[0][1]
|
||||
|
||||
# instantiate the extension with its metadata and add to dict
|
||||
ext_instance: BaseExtension = ext_class(metadata)
|
||||
@@ -72,7 +76,7 @@ class ExtensionsMenu(BaseMenu):
|
||||
except (IOError, json.JSONDecodeError, ImportError) as e:
|
||||
print(f"Failed loading extension {ext}: {e}")
|
||||
|
||||
return dict(sorted(ext_dict.items()))
|
||||
return dict(sorted(ext_dict.items(), key=lambda x: int(x[0])))
|
||||
|
||||
def extension_submenu(self, **kwargs):
|
||||
ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run()
|
||||
|
||||
@@ -132,7 +132,7 @@ class MoonrakerObico:
|
||||
raise
|
||||
env_file_content = env_template_file_content.replace(
|
||||
"%CFG%",
|
||||
f"{self.base.cfg_dir}/{self.cfg_file}",
|
||||
f"{self.cfg_file}",
|
||||
)
|
||||
return env_file_content
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import List
|
||||
from components.klipper.klipper import Klipper
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from core.instance_manager.instance_manager import InstanceManager
|
||||
from core.instance_manager.base_instance import SUFFIX_BLACKLIST
|
||||
from core.logger import DialogType, Logger
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
@@ -308,7 +309,8 @@ class ObicoExtension(BaseExtension):
|
||||
def _check_and_opt_link_instances(self) -> None:
|
||||
Logger.print_status("Checking link status of Obico instances ...")
|
||||
|
||||
ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico)
|
||||
suffix_blacklist: List[str] = [suffix for suffix in SUFFIX_BLACKLIST if suffix != 'obico']
|
||||
ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico, suffix_blacklist=suffix_blacklist)
|
||||
unlinked_instances: List[MoonrakerObico] = [
|
||||
obico for obico in ob_instances if not obico.is_linked
|
||||
]
|
||||
|
||||
28
kiauh/extensions/octoapp/__init__.py
Normal file
28
kiauh/extensions/octoapp/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# ======================================================================= #
|
||||
# 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 pathlib import Path
|
||||
|
||||
# repo
|
||||
OA_REPO = "https://github.com/crysxd/OctoApp-Plugin.git"
|
||||
|
||||
# directories
|
||||
OA_DIR = Path.home().joinpath("octoapp")
|
||||
OA_ENV_DIR = Path.home().joinpath("octoapp-env")
|
||||
|
||||
# files
|
||||
OA_REQ_FILE = OA_DIR.joinpath("requirements.txt")
|
||||
OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json")
|
||||
OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh")
|
||||
OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh")
|
||||
OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log")
|
||||
|
||||
# filenames
|
||||
OA_CFG_NAME = "octoapp.conf"
|
||||
OA_LOG_NAME = "octoapp.log"
|
||||
OA_SYS_CFG_NAME = "octoapp-system.cfg"
|
||||
17
kiauh/extensions/octoapp/metadata.json
Normal file
17
kiauh/extensions/octoapp/metadata.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"metadata": {
|
||||
"index": 9,
|
||||
"module": "octoapp_extension",
|
||||
"maintained_by": "crysxd",
|
||||
"display_name": "OctoApp for Klipper",
|
||||
"description": [
|
||||
"Your favorite 3D printing app for iOS & Android",
|
||||
"- Print notifications on your phone & watch",
|
||||
"- Control and start prints from your phone",
|
||||
"- Live webcam view",
|
||||
"- Live Gcode preview",
|
||||
"- And much much more!"
|
||||
],
|
||||
"updates": true
|
||||
}
|
||||
}
|
||||
75
kiauh/extensions/octoapp/octoapp.py
Normal file
75
kiauh/extensions/octoapp/octoapp.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# ======================================================================= #
|
||||
# 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 __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError, run
|
||||
|
||||
from components.moonraker import MOONRAKER_CFG_NAME
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from core.instance_manager.base_instance import BaseInstance
|
||||
from core.logger import Logger
|
||||
from extensions.octoapp import (
|
||||
OA_CFG_NAME,
|
||||
OA_DIR,
|
||||
OA_ENV_DIR,
|
||||
OA_INSTALL_SCRIPT,
|
||||
OA_LOG_NAME,
|
||||
OA_SYS_CFG_NAME,
|
||||
OA_UPDATE_SCRIPT,
|
||||
)
|
||||
from utils.sys_utils import get_service_file_path
|
||||
|
||||
|
||||
@dataclass
|
||||
class Octoapp:
|
||||
suffix: str
|
||||
base: BaseInstance = field(init=False, repr=False)
|
||||
service_file_path: Path = field(init=False)
|
||||
log_file_name = OA_LOG_NAME
|
||||
dir: Path = OA_DIR
|
||||
env_dir: Path = OA_ENV_DIR
|
||||
data_dir: Path = field(init=False)
|
||||
store_dir: Path = field(init=False)
|
||||
cfg_file: Path = field(init=False)
|
||||
sys_cfg_file: Path = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.base: BaseInstance = BaseInstance(Moonraker, self.suffix)
|
||||
self.base.log_file_name = self.log_file_name
|
||||
|
||||
self.service_file_path: Path = get_service_file_path(
|
||||
Octoapp, self.suffix
|
||||
)
|
||||
self.store_dir = self.base.data_dir.joinpath("store")
|
||||
self.cfg_file = self.base.cfg_dir.joinpath(OA_CFG_NAME)
|
||||
self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME)
|
||||
self.data_dir = self.base.data_dir
|
||||
self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME)
|
||||
|
||||
def create(self) -> None:
|
||||
Logger.print_status("Creating OctoApp for Klipper Instance ...")
|
||||
|
||||
try:
|
||||
cmd = f"{OA_INSTALL_SCRIPT} {self.base.cfg_dir}/{MOONRAKER_CFG_NAME}"
|
||||
run(cmd, check=True, shell=True)
|
||||
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error(f"Error creating instance: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def update() -> None:
|
||||
try:
|
||||
run(OA_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OA_DIR)
|
||||
|
||||
except CalledProcessError as e:
|
||||
Logger.print_error(f"Error updating OctoApp for Klipper: {e}")
|
||||
raise
|
||||
208
kiauh/extensions/octoapp/octoapp_extension.py
Normal file
208
kiauh/extensions/octoapp/octoapp_extension.py
Normal file
@@ -0,0 +1,208 @@
|
||||
# ======================================================================= #
|
||||
# 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 json
|
||||
from typing import List
|
||||
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from components.klipper.klipper import Klipper
|
||||
from core.instance_manager.instance_manager import InstanceManager
|
||||
from core.logger import DialogType, Logger
|
||||
from extensions.base_extension import BaseExtension
|
||||
from extensions.octoapp import (
|
||||
OA_DEPS_JSON_FILE,
|
||||
OA_DIR,
|
||||
OA_ENV_DIR,
|
||||
OA_INSTALL_SCRIPT,
|
||||
OA_INSTALLER_LOG_FILE,
|
||||
OA_REPO,
|
||||
OA_REQ_FILE,
|
||||
OA_SYS_CFG_NAME,
|
||||
)
|
||||
from extensions.octoapp.octoapp import Octoapp
|
||||
from utils.common import (
|
||||
check_install_dependencies,
|
||||
moonraker_exists,
|
||||
)
|
||||
from utils.config_utils import (
|
||||
remove_config_section,
|
||||
)
|
||||
from utils.fs_utils import run_remove_routines
|
||||
from utils.git_utils import git_clone_wrapper
|
||||
from utils.input_utils import get_confirm
|
||||
from utils.instance_utils import get_instances
|
||||
from utils.sys_utils import (
|
||||
install_python_requirements,
|
||||
parse_packages_from_file,
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class OctoappExtension(BaseExtension):
|
||||
def install_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Installing OctoApp for Klipper ...")
|
||||
|
||||
# check if moonraker is installed. if not, notify the user and exit
|
||||
if not moonraker_exists():
|
||||
return
|
||||
|
||||
force_clone = False
|
||||
OA_instances: List[Octoapp] = get_instances(Octoapp)
|
||||
if OA_instances:
|
||||
Logger.print_dialog(
|
||||
DialogType.INFO,
|
||||
[
|
||||
"OctoApp is already installed!",
|
||||
"It is safe to run the installer again to link your "
|
||||
"printer or repair any issues.",
|
||||
],
|
||||
)
|
||||
if not get_confirm("Re-run OctoApp installation?"):
|
||||
Logger.print_info("Exiting OctoApp for Klipper installation ...")
|
||||
return
|
||||
else:
|
||||
Logger.print_status("Re-Installing OctoApp for Klipper ...")
|
||||
force_clone = True
|
||||
|
||||
mr_instances: List[Moonraker] = get_instances(Moonraker)
|
||||
|
||||
mr_names = [f"● {moonraker.data_dir.name}" for moonraker in mr_instances]
|
||||
if len(mr_names) > 1:
|
||||
Logger.print_dialog(
|
||||
DialogType.INFO,
|
||||
[
|
||||
"The following Moonraker instances were found:",
|
||||
*mr_names,
|
||||
"\n\n",
|
||||
"The setup will apply the same names to OctoApp!",
|
||||
],
|
||||
)
|
||||
|
||||
if not get_confirm(
|
||||
"Continue OctoApp for Klipper installation?",
|
||||
default_choice=True,
|
||||
allow_go_back=True,
|
||||
):
|
||||
Logger.print_info("Exiting OctoApp for Klipper installation ...")
|
||||
return
|
||||
|
||||
try:
|
||||
git_clone_wrapper(OA_REPO, OA_DIR, force=force_clone)
|
||||
|
||||
for moonraker in mr_instances:
|
||||
instance = Octoapp(suffix=moonraker.suffix)
|
||||
instance.create()
|
||||
|
||||
InstanceManager.restart_all(mr_instances)
|
||||
|
||||
Logger.print_dialog(
|
||||
DialogType.SUCCESS,
|
||||
["OctoApp for Klipper successfully installed!"],
|
||||
center_content=True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
Logger.print_error(
|
||||
f"Error during OctoApp for Klipper installation:\n{e}"
|
||||
)
|
||||
|
||||
def update_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Updating OctoApp for Klipper ...")
|
||||
try:
|
||||
Octoapp.update()
|
||||
Logger.print_dialog(
|
||||
DialogType.SUCCESS,
|
||||
["OctoApp for Klipper successfully updated!"],
|
||||
center_content=True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
Logger.print_error(f"Error during OctoApp for Klipper update:\n{e}")
|
||||
|
||||
def remove_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Removing OctoApp for Klipper ...")
|
||||
|
||||
mr_instances: List[Moonraker] = get_instances(Moonraker)
|
||||
ob_instances: List[Octoapp] = get_instances(Octoapp)
|
||||
|
||||
try:
|
||||
self._remove_OA_instances(ob_instances)
|
||||
self._remove_OA_store_dirs()
|
||||
self._remove_OA_dir()
|
||||
self._remove_OA_env()
|
||||
remove_config_section(f"include {OA_SYS_CFG_NAME}", mr_instances)
|
||||
run_remove_routines(OA_INSTALLER_LOG_FILE)
|
||||
Logger.print_dialog(
|
||||
DialogType.SUCCESS,
|
||||
["OctoApp for Klipper successfully removed!"],
|
||||
center_content=True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
Logger.print_error(f"Error during OctoApp for Klipper removal:\n{e}")
|
||||
|
||||
def _install_OA_dependencies(self) -> None:
|
||||
OA_deps = []
|
||||
if OA_DEPS_JSON_FILE.exists():
|
||||
with open(OA_DEPS_JSON_FILE, "r") as deps:
|
||||
OA_deps = json.load(deps).get("debian", [])
|
||||
elif OA_INSTALL_SCRIPT.exists():
|
||||
OA_deps = parse_packages_from_file(OA_INSTALL_SCRIPT)
|
||||
|
||||
if not OA_deps:
|
||||
raise ValueError("Error reading OctoApp dependencies!")
|
||||
|
||||
check_install_dependencies({*OA_deps})
|
||||
install_python_requirements(OA_ENV_DIR, OA_REQ_FILE)
|
||||
|
||||
def _remove_OA_instances(
|
||||
self,
|
||||
instance_list: List[Octoapp],
|
||||
) -> None:
|
||||
if not instance_list:
|
||||
Logger.print_info("No OctoApp instances found. Skipped ...")
|
||||
return
|
||||
|
||||
for instance in instance_list:
|
||||
Logger.print_status(
|
||||
f"Removing instance {instance.service_file_path.stem} ..."
|
||||
)
|
||||
InstanceManager.remove(instance)
|
||||
|
||||
def _remove_OA_dir(self) -> None:
|
||||
Logger.print_status("Removing OctoApp for Klipper directory ...")
|
||||
|
||||
if not OA_DIR.exists():
|
||||
Logger.print_info(f"'{OA_DIR}' does not exist. Skipped ...")
|
||||
return
|
||||
|
||||
run_remove_routines(OA_DIR)
|
||||
|
||||
|
||||
def _remove_OA_store_dirs(self) -> None:
|
||||
Logger.print_status("Removing OctoApp for Klipper store directory ...")
|
||||
|
||||
klipper_instances: List[Moonraker] = get_instances(Klipper)
|
||||
|
||||
for instance in klipper_instances:
|
||||
store_dir = instance.data_dir.joinpath("octoapp-store")
|
||||
if not store_dir.exists():
|
||||
Logger.print_info(f"'{store_dir}' does not exist. Skipped ...")
|
||||
return
|
||||
|
||||
run_remove_routines(store_dir)
|
||||
|
||||
|
||||
def _remove_OA_env(self) -> None:
|
||||
Logger.print_status("Removing OctoApp for Klipper environment ...")
|
||||
|
||||
if not OA_ENV_DIR.exists():
|
||||
Logger.print_info(f"'{OA_ENV_DIR}' does not exist. Skipped ...")
|
||||
return
|
||||
|
||||
run_remove_routines(OA_ENV_DIR)
|
||||
0
kiauh/extensions/simply_print/__init__.py
Normal file
0
kiauh/extensions/simply_print/__init__.py
Normal file
13
kiauh/extensions/simply_print/metadata.json
Normal file
13
kiauh/extensions/simply_print/metadata.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"metadata": {
|
||||
"index": 10,
|
||||
"module": "simply_print_extension",
|
||||
"maintained_by": "dw-0",
|
||||
"display_name": "SimplyPrint",
|
||||
"description": [
|
||||
"3D Printer Cloud Management Software.",
|
||||
"\n\n",
|
||||
"3D printing doesn't have to be a complicated, analog, SD card-filled experience; step into the future of modern 3D printing"
|
||||
]
|
||||
}
|
||||
}
|
||||
131
kiauh/extensions/simply_print/simply_print_extension.py
Normal file
131
kiauh/extensions/simply_print/simply_print_extension.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# ======================================================================= #
|
||||
# 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 typing import List
|
||||
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from core.instance_manager.instance_manager import InstanceManager
|
||||
from core.logger import DialogType, Logger
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from extensions.base_extension import BaseExtension
|
||||
from utils.common import backup_printer_config_dir, moonraker_exists
|
||||
from utils.input_utils import get_confirm
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class SimplyPrintExtension(BaseExtension):
|
||||
def install_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Installing SimplyPrint ...")
|
||||
|
||||
if not (mr_instances := moonraker_exists("SimplyPrint Installer")):
|
||||
return
|
||||
|
||||
Logger.print_dialog(
|
||||
DialogType.INFO,
|
||||
self._construct_dialog(mr_instances, True),
|
||||
)
|
||||
|
||||
if not get_confirm(
|
||||
"Continue SimplyPrint installation?",
|
||||
default_choice=True,
|
||||
allow_go_back=True,
|
||||
):
|
||||
Logger.print_info("Exiting SimplyPrint installation ...")
|
||||
return
|
||||
|
||||
try:
|
||||
self._patch_moonraker_confs(mr_instances, True)
|
||||
|
||||
except Exception as e:
|
||||
Logger.print_error(f"Error during SimplyPrint installation:\n{e}")
|
||||
|
||||
def remove_extension(self, **kwargs) -> None:
|
||||
Logger.print_status("Removing SimplyPrint ...")
|
||||
|
||||
if not (mr_instances := moonraker_exists("SimplyPrint Uninstaller")):
|
||||
return
|
||||
|
||||
Logger.print_dialog(
|
||||
DialogType.INFO,
|
||||
self._construct_dialog(mr_instances, False),
|
||||
)
|
||||
|
||||
if not get_confirm(
|
||||
"Do you really want to uninstall SimplyPrint?",
|
||||
default_choice=True,
|
||||
allow_go_back=True,
|
||||
):
|
||||
Logger.print_info("Exiting SimplyPrint uninstallation ...")
|
||||
return
|
||||
|
||||
try:
|
||||
self._patch_moonraker_confs(mr_instances, False)
|
||||
|
||||
except Exception as e:
|
||||
Logger.print_error(f"Error during SimplyPrint installation:\n{e}")
|
||||
|
||||
def _construct_dialog(
|
||||
self, mr_instances: List[Moonraker], is_install: bool
|
||||
) -> List[str]:
|
||||
mr_names = [f"● {m.service_file_path.name}" for m in mr_instances]
|
||||
_type = "install" if is_install else "uninstall"
|
||||
|
||||
return [
|
||||
"The following Moonraker instances were found:",
|
||||
*mr_names,
|
||||
"\n\n",
|
||||
f"The setup will {_type} SimplyPrint for all Moonraker instances. "
|
||||
f"After {_type}ation, all Moonraker services will be restarted!",
|
||||
]
|
||||
|
||||
def _patch_moonraker_confs(
|
||||
self, mr_instances: List[Moonraker], is_install: bool
|
||||
) -> None:
|
||||
section = "simplyprint"
|
||||
_type, _ft = ("Adding", "to") if is_install else ("Removing", "from")
|
||||
|
||||
patched_files = []
|
||||
for moonraker in mr_instances:
|
||||
Logger.print_status(
|
||||
f"{_type} section 'simplyprint' {_ft} {moonraker.cfg_file} ..."
|
||||
)
|
||||
scp = SimpleConfigParser()
|
||||
scp.read_file(moonraker.cfg_file)
|
||||
|
||||
install_and_has_section = is_install and scp.has_section(section)
|
||||
uninstall_and_has_no_section = not is_install and not scp.has_section(
|
||||
section
|
||||
)
|
||||
|
||||
if install_and_has_section or uninstall_and_has_no_section:
|
||||
status = "already" if is_install else "does not"
|
||||
Logger.print_info(
|
||||
f"Section 'simplyprint' {status} exists! Skipping ..."
|
||||
)
|
||||
continue
|
||||
|
||||
if is_install and not scp.has_section("simplyprint"):
|
||||
backup_printer_config_dir()
|
||||
scp.add_section(section)
|
||||
elif not is_install and scp.has_section("simplyprint"):
|
||||
backup_printer_config_dir()
|
||||
scp.remove_section(section)
|
||||
scp.write_file(moonraker.cfg_file)
|
||||
patched_files.append(moonraker.cfg_file)
|
||||
|
||||
if patched_files:
|
||||
InstanceManager.restart_all(mr_instances)
|
||||
|
||||
install_state = "successfully" if patched_files else "was already"
|
||||
Logger.print_dialog(
|
||||
DialogType.SUCCESS,
|
||||
[f"SimplyPrint {install_state} {'' if is_install else 'un'}installed!"],
|
||||
center_content=True,
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Moonraker Telegram Bot SV1 %INST%
|
||||
Description=Moonraker Telegram Bot SV1
|
||||
Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki
|
||||
After=network-online.target
|
||||
|
||||
|
||||
@@ -161,10 +161,11 @@ class TelegramBotExtension(BaseExtension):
|
||||
# install dependencies
|
||||
script = TG_BOT_DIR.joinpath("scripts/install.sh")
|
||||
package_list = parse_packages_from_file(script)
|
||||
|
||||
check_install_dependencies({*package_list})
|
||||
|
||||
# create virtualenv
|
||||
if create_python_venv(TG_BOT_ENV):
|
||||
if create_python_venv(TG_BOT_ENV, allow_access_to_system_site_packages=True):
|
||||
install_python_requirements(TG_BOT_ENV, TG_BOT_REQ_FILE)
|
||||
|
||||
def _patch_bot_update_manager(self, instances: List[Moonraker]) -> None:
|
||||
|
||||
@@ -14,10 +14,11 @@ from pathlib import Path
|
||||
from typing import Dict, List, Literal, Optional, Set
|
||||
|
||||
from components.klipper.klipper import Klipper
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from core.constants import (
|
||||
COLOR_CYAN,
|
||||
GLOBAL_DEPS,
|
||||
PRINTER_CFG_BACKUP_DIR,
|
||||
PRINTER_DATA_BACKUP_DIR,
|
||||
RESET_FORMAT,
|
||||
)
|
||||
from core.logger import DialogType, Logger
|
||||
@@ -142,23 +143,25 @@ def backup_printer_config_dir() -> None:
|
||||
instances: List[Klipper] = get_instances(Klipper)
|
||||
bm = BackupManager()
|
||||
|
||||
if not instances:
|
||||
Logger.print_info("Unable to find directory to backup!")
|
||||
Logger.print_info("Are there no Klipper instances installed?")
|
||||
return
|
||||
|
||||
for instance in instances:
|
||||
name = f"config-{instance.data_dir.name}"
|
||||
bm.backup_directory(
|
||||
name,
|
||||
instance.data_dir.name,
|
||||
source=instance.base.cfg_dir,
|
||||
target=PRINTER_CFG_BACKUP_DIR,
|
||||
target=PRINTER_DATA_BACKUP_DIR,
|
||||
)
|
||||
|
||||
|
||||
def moonraker_exists(name: str = "") -> bool:
|
||||
def moonraker_exists(name: str = "") -> List[Moonraker]:
|
||||
"""
|
||||
Helper method to check if a Moonraker instance exists
|
||||
:param name: Optional name of an installer where the check is performed
|
||||
:return: True if at least one Moonraker instance exists, False otherwise
|
||||
"""
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
|
||||
mr_instances: List[Moonraker] = get_instances(Moonraker)
|
||||
|
||||
info = (
|
||||
@@ -175,8 +178,8 @@ def moonraker_exists(name: str = "") -> bool:
|
||||
f"{info}. Please install Moonraker first!",
|
||||
],
|
||||
)
|
||||
return False
|
||||
return True
|
||||
return []
|
||||
return mr_instances
|
||||
|
||||
|
||||
def trunc_string(input_str: str, length: int) -> str:
|
||||
|
||||
@@ -48,6 +48,8 @@ def add_config_section(
|
||||
|
||||
scp.write_file(cfg_file)
|
||||
|
||||
Logger.print_ok("OK!")
|
||||
|
||||
|
||||
def add_config_section_at_top(section: str, instances: List[InstanceType]) -> None:
|
||||
# TODO: this could be implemented natively in SimpleConfigParser
|
||||
@@ -69,6 +71,8 @@ def add_config_section_at_top(section: str, instances: List[InstanceType]) -> No
|
||||
cfg_file.unlink()
|
||||
tmp_cfg_path.rename(cfg_file)
|
||||
|
||||
Logger.print_ok("OK!")
|
||||
|
||||
|
||||
def remove_config_section(section: str, instances: List[InstanceType]) -> None:
|
||||
for instance in instances:
|
||||
@@ -87,3 +91,5 @@ def remove_config_section(section: str, instances: List[InstanceType]) -> None:
|
||||
|
||||
scp.remove_section(section)
|
||||
scp.write_file(cfg_file)
|
||||
|
||||
Logger.print_ok("OK!")
|
||||
|
||||
@@ -7,7 +7,7 @@ from http.client import HTTPResponse
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run
|
||||
from typing import List, Type
|
||||
from typing import List, Tuple, Type
|
||||
|
||||
from core.instance_manager.instance_manager import InstanceManager
|
||||
from core.logger import Logger
|
||||
@@ -70,7 +70,7 @@ def git_pull_wrapper(repo: str, target_dir: Path) -> None:
|
||||
return
|
||||
|
||||
|
||||
def get_repo_name(repo: Path) -> tuple[str, str] | None:
|
||||
def get_repo_name(repo: Path) -> Tuple[str, str]:
|
||||
"""
|
||||
Helper method to extract the organisation and name of a repository |
|
||||
:param repo: repository to extract the values from
|
||||
@@ -83,11 +83,14 @@ def get_repo_name(repo: Path) -> tuple[str, str] | None:
|
||||
cmd = ["git", "-C", repo.as_posix(), "config", "--get", "remote.origin.url"]
|
||||
result: str = check_output(cmd, stderr=DEVNULL).decode(encoding="utf-8")
|
||||
substrings: List[str] = result.strip().split("/")[-2:]
|
||||
return substrings[0], substrings[1]
|
||||
|
||||
# return "/".join(substrings).replace(".git", "")
|
||||
orga: str = substrings[0] if substrings[0] else "-"
|
||||
name: str = substrings[1] if substrings[1] else "-"
|
||||
|
||||
return orga, name
|
||||
|
||||
except CalledProcessError:
|
||||
return None
|
||||
return "-", "-"
|
||||
|
||||
|
||||
def get_local_tags(repo_path: Path, _filter: str | None = None) -> List[str]:
|
||||
@@ -184,7 +187,7 @@ def compare_semver_tags(tag1: str, tag2: str) -> bool:
|
||||
if tag1 == tag2:
|
||||
return False
|
||||
|
||||
def parse_version(v):
|
||||
def parse_version(v) -> List[int]:
|
||||
return list(map(int, v[1:].split(".")))
|
||||
|
||||
tag1_parts = parse_version(tag1)
|
||||
|
||||
@@ -13,6 +13,7 @@ from components.klipper.klipper import Klipper
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
from extensions.obico.moonraker_obico import MoonrakerObico
|
||||
from extensions.octoeverywhere.octoeverywhere import Octoeverywhere
|
||||
from extensions.octoapp.octoapp import Octoapp
|
||||
from extensions.telegram_bot.moonraker_telegram_bot import MoonrakerTelegramBot
|
||||
|
||||
InstanceType = TypeVar(
|
||||
@@ -22,4 +23,5 @@ InstanceType = TypeVar(
|
||||
MoonrakerTelegramBot,
|
||||
MoonrakerObico,
|
||||
Octoeverywhere,
|
||||
Octoapp,
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ from core.instance_manager.base_instance import SUFFIX_BLACKLIST
|
||||
from utils.instance_type import InstanceType
|
||||
|
||||
|
||||
def get_instances(instance_type: type) -> List[InstanceType]:
|
||||
def get_instances(instance_type: type, suffix_blacklist: List[str] = SUFFIX_BLACKLIST) -> List[InstanceType]:
|
||||
from utils.common import convert_camelcase_to_kebabcase
|
||||
|
||||
if not isinstance(instance_type, type):
|
||||
@@ -30,7 +30,7 @@ def get_instances(instance_type: type) -> List[InstanceType]:
|
||||
Path(SYSTEMD, service)
|
||||
for service in SYSTEMD.iterdir()
|
||||
if pattern.search(service.name)
|
||||
and not any(s in service.name for s in SUFFIX_BLACKLIST)
|
||||
and not any(s in service.name for s in suffix_blacklist)
|
||||
]
|
||||
|
||||
instance_list = [
|
||||
|
||||
@@ -91,19 +91,27 @@ def parse_packages_from_file(source_file: Path) -> List[str]:
|
||||
return packages
|
||||
|
||||
|
||||
def create_python_venv(target: Path, force: bool = False) -> bool:
|
||||
def create_python_venv(
|
||||
target: Path,
|
||||
force: bool = False,
|
||||
allow_access_to_system_site_packages: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Create a python 3 virtualenv at the provided target destination.
|
||||
Returns True if the virtualenv was created successfully.
|
||||
Returns False if the virtualenv already exists, recreation was declined or creation failed.
|
||||
:param force: Force recreation of the virtualenv
|
||||
:param target: Path where to create the virtualenv at
|
||||
:param force: Force recreation of the virtualenv
|
||||
:param allow_access_to_system_site_packages: give the virtual environment access to the system site-packages dir
|
||||
:return: bool
|
||||
"""
|
||||
Logger.print_status("Set up Python virtual environment ...")
|
||||
cmd = ["virtualenv", "-p", "/usr/bin/python3", target.as_posix()]
|
||||
cmd.append(
|
||||
"--system-site-packages"
|
||||
) if allow_access_to_system_site_packages else None
|
||||
if not target.exists():
|
||||
try:
|
||||
cmd = ["virtualenv", "-p", "/usr/bin/python3", target.as_posix()]
|
||||
run(cmd, check=True)
|
||||
Logger.print_ok("Setup of virtualenv successful!")
|
||||
return True
|
||||
|
||||
@@ -127,9 +127,9 @@ managed_services: Spoolman
|
||||
regex="${HOME//\//\\/}\/([A-Za-z0-9_]+)\/moonraker\.asvc"
|
||||
moonraker_asvc=$(find "${HOME}" -maxdepth 2 -type f -regextype posix-extended -regex "${regex}" | sort)
|
||||
|
||||
if [[ -n ${moonraker_asvc} ]]; then
|
||||
if ! grep -q "^Spoolman$" "${moonraker_asvc}"; then
|
||||
status_msg "Adding Spoolman service to moonraker.asvc..."
|
||||
/bin/sh -c "echo 'Spoolman' >> ${moonraker_asvc}"
|
||||
sed -i '$a''Spoolman' "${moonraker_asvc}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ function get_spoolman_status() {
|
||||
|
||||
function get_local_spoolman_version() {
|
||||
[[ ! -d "${SPOOLMAN_DIR}" ]] && return
|
||||
|
||||
|
||||
local version
|
||||
version=$(grep -o '"version":\s*"[^"]*' "${SPOOLMAN_DIR}"/release_info.json | cut -d'"' -f4)
|
||||
echo "${version}"
|
||||
|
||||
Reference in New Issue
Block a user