Compare commits

..

17 Commits

Author SHA1 Message Date
Cody D Dixon
2d3548b5f3 Merge 6c81ba2232 into 89b48168f4 2025-09-06 11:20:23 -07:00
dw-0
89b48168f4 fix: do not drop SAVE_CONFIG block when editing and writing config files (#723)
Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 4a6e5f2..f5eee99

f5eee99 feat: add support for parsing and handling `SAVE_CONFIG` blocks (#4)
8170583 refactor(api)!: `getval` now returns a string, `getvals` returns list of strings

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: f5eee99b0f04717c6bbf30c1256d70ad019223d5
2025-09-06 13:12:20 +02:00
CodeMasterCody3D
6c81ba2232 Merge branch 'master' of https://github.com/CodeMasterCody3D/kiauhPlusDroidKlipp 2025-01-21 05:23:03 -06:00
CodeMasterCody3D
a777461296 yes 2025-01-21 05:22:16 -06:00
CodeMasterCody3D
2c045bb647 Update droidklipp.py
forgot to add imports
2025-01-21 04:11:03 -06:00
CodeMasterCody3D
a21e059328 Update droidklipp.py
fixed folder creating and bug
2025-01-21 04:08:48 -06:00
CodeMasterCody3D
6853e97fb8 Update droidklipp.py
fixed folder creation
2025-01-21 04:05:10 -06:00
CodeMasterCody3D
837488e4dd Update install_menu.py 2025-01-21 03:59:36 -06:00
CodeMasterCody3D
68cc03f3d0 Merge branch 'master' of https://github.com/CodeMasterCody3D/kiauhPlusDroidKlipp 2025-01-21 03:51:23 -06:00
CodeMasterCody3D
606686b33c droidklipp 2025-01-21 03:40:50 -06:00
CodeMasterCody3D
fc494e21da Update main_menu.py
droidklipp
2025-01-21 03:25:46 -06:00
CodeMasterCody3D
41fccb88fd Update common.py
droidklipp
2025-01-21 03:24:45 -06:00
CodeMasterCody3D
66975cd913 Update README.md
updated readme to add DroidKlipp
2025-01-21 03:13:08 -06:00
CodeMasterCody3D
89ad92468d Update README.md
install update
2025-01-21 03:09:55 -06:00
CodeMasterCody3D
ef44ba8253 droidklipp
droidklipp
2025-01-21 03:07:03 -06:00
CodeMasterCody3D
d41865e693 Update install_menu.py
added DroidKlipp
2025-01-21 03:06:16 -06:00
CodeMasterCody3D
ba594355ba Update install_menu.py 2025-01-21 02:15:34 -06:00
20 changed files with 346 additions and 12 deletions

3
.vs/ProjectSettings.json Normal file
View File

@@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

11
.vs/VSWorkspaceState.json Normal file
View File

@@ -0,0 +1,11 @@
{
"ExpandedNodes": [
"",
"\\kiauh",
"\\kiauh\\components\\droidklipp",
"\\kiauh\\core",
"\\kiauh\\core\\menus"
],
"SelectedNode": "\\kiauh\\core\\menus\\main_menu.py",
"PreviewInSolutionExplorer": false
}

Binary file not shown.

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

View File

@@ -71,14 +71,14 @@ sudo apt-get update && sudo apt-get install git -y
Once git is installed, use the following command to download KIAUH into your home-directory:
```shell
cd ~ && git clone https://github.com/dw-0/kiauh.git
cd ~ && git clone https://github.com/CodeMasterCody3D/kiauhPlusDroidKlipp.git
```
* **Step 3:** \
Finally, start KIAUH by running the next command:
```shell
./kiauh/kiauh.sh
./kiauhPlusDroidKlipp/kiauh.sh
```
* **Step 4:** \

View File

@@ -0,0 +1,47 @@
import os
import subprocess
def install_droidklipp():
try:
print("Are you sure you want to install DroidKlipp? (Y/N)")
user_confirmation = input().strip().lower()
if user_confirmation != 'y':
print("DroidKlipp installation aborted.")
return
print("Installing DroidKlipp...")
subprocess.run(['sudo', 'apt', 'install', '-y', 'adb', 'tmux'], check=True)
# Define the DroidKlipp repository URL and directory
droidklipp_repo_url = "https://github.com/CodeMasterCody3D/DroidKlipp.git"
droidklipp_dir = os.path.expanduser('~/DroidKlipp')
# Check if DroidKlipp directory exists, if not create it
if not os.path.isdir(droidklipp_dir):
print("DroidKlipp folder not found, creating directory...")
os.makedirs(droidklipp_dir)
# Clone the repository if not already cloned
if not os.path.isdir(os.path.join(droidklipp_dir, '.git')):
print("Cloning the DroidKlipp repository...")
subprocess.run(['git', 'clone', droidklipp_repo_url, droidklipp_dir], check=True)
else:
print("DroidKlipp repository already exists.")
# Change to the DroidKlipp directory
os.chdir(droidklipp_dir)
# Set executable permissions for the installation script
subprocess.run(['sudo', 'chmod', '+x', 'droidklipp.sh'], check=True)
# Run the installation script
subprocess.run(['./droidklipp.sh'], check=True)
print("DroidKlipp installation complete!")
except subprocess.CalledProcessError as e:
print(f"Error during installation: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
# Ensure you call this with proper confirmation before installation

View File

@@ -11,6 +11,7 @@ from __future__ import annotations
import textwrap
from typing import Type
from components.droidklipp.droidklipp import install_droidklipp
from components.crowsnest.crowsnest import install_crowsnest
from components.klipper.services.klipper_setup_service import KlipperSetupService
from components.klipperscreen.klipperscreen import install_klipperscreen
@@ -41,7 +42,6 @@ class InstallMenu(BaseMenu):
def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None:
from core.menus.main_menu import MainMenu
self.previous_menu = previous_menu if previous_menu is not None else MainMenu
def set_options(self) -> None:
@@ -53,7 +53,9 @@ class InstallMenu(BaseMenu):
"5": Option(method=self.install_mainsail_config),
"6": Option(method=self.install_fluidd_config),
"7": Option(method=self.install_klipperscreen),
"8": Option(method=self.install_crowsnest),
"8": Option(method=self.install_droidklipp), # Add DroidKlipp option
"9": Option(method=self.install_crowsnest),
}
def print_menu(self) -> None:
@@ -62,15 +64,17 @@ class InstallMenu(BaseMenu):
╟───────────────────────────┬───────────────────────────╢
║ Firmware & API: │ Touchscreen GUI: ║
║ 1) [Klipper] │ 7) [KlipperScreen] ║
║ 2) [Moonraker] │
║ 2) [Moonraker] │ 8) [DroidKlipp]
║ │ ║
║ │ Webcam Streamer: ║
║ Webinterface: │ 8) [Crowsnest] ║
║ Webinterface: │ 9) [Crowsnest] ║
║ 3) [Mainsail] │ ║
║ 4) [Fluidd] │ ║
║ │ ║
║ Client-Config: │ ║
║ 5) [Mainsail-Config] │ ║
║ 6) [Fluidd-Config] │ ║
║ │ ║
╟───────────────────────────┴───────────────────────────╢
"""
)[1:]
@@ -107,3 +111,6 @@ class InstallMenu(BaseMenu):
def install_crowsnest(self, **kwargs) -> None:
install_crowsnest()
def install_droidklipp(self, **kwargs) -> None:
install_droidklipp()

View File

@@ -12,6 +12,7 @@ import sys
import textwrap
from typing import Callable, Type
from components.droidklipp.droidklipp import install_droidklipp
from components.crowsnest.crowsnest import get_crowsnest_status
from components.klipper.klipper_utils import get_klipper_status
from components.klipperscreen.klipperscreen import get_klipperscreen_status

View File

@@ -12,6 +12,7 @@ Specialized for handling Klipper style config files.
- Option Block: A line starting with a word, followed by a `:` or `=` and a newline
- Comment: A line starting with a `#` or `;`
- Blank: A line containing only whitespace characters
- SaveConfig: Klippers auto-generated SAVE_CONFIG section that can be found at the very end of the config file
---

View File

@@ -49,6 +49,9 @@ LINE_COMMENT_RE = re.compile(r"^\s*[#;].*")
# - the line MUST contain only whitespace characters
EMPTY_LINE_RE = re.compile(r"^\s*$")
SAVE_CONFIG_START_RE = re.compile(r"^#\*# <-+ SAVE_CONFIG -+>$")
SAVE_CONFIG_CONTENT_RE = re.compile(r"^#\*#.*$")
BOOLEAN_STATES = {
"1": True,
"yes": True,

View File

@@ -18,7 +18,7 @@ from ..simple_config_parser.constants import (
LINE_COMMENT_RE,
OPTION_RE,
OPTIONS_BLOCK_START_RE,
SECTION_RE, LineType, INDENT,
SECTION_RE, LineType, INDENT, SAVE_CONFIG_START_RE, SAVE_CONFIG_CONTENT_RE,
)
_UNSET = object()
@@ -61,25 +61,34 @@ class SimpleConfigParser:
def __init__(self) -> None:
self.header: List[str] = []
self.save_config_block: List[str] = []
self.config: Dict = {}
self.current_section: str | None = None
self.current_opt_block: str | None = None
self.in_option_block: bool = False
def _match_section(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of a section"""
"""Whether the given line matches the definition of a section"""
return SECTION_RE.match(line) is not None
def _match_option(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of an option"""
"""Whether the given line matches the definition of an option"""
return OPTION_RE.match(line) is not None
def _match_options_block_start(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of a multiline option"""
"""Whether the given line matches the definition of a multiline option"""
return OPTIONS_BLOCK_START_RE.match(line) is not None
def _match_save_config_start(self, line: str) -> bool:
"""Whether the given line matches the definition of a save config start"""
return SAVE_CONFIG_START_RE.match(line) is not None
def _match_save_config_content(self, line: str) -> bool:
"""Whether the given line matches the definition of a save config content"""
return SAVE_CONFIG_CONTENT_RE.match(line) is not None
def _match_line_comment(self, line: str) -> bool:
"""Wheter or not the given line matches the definition of a comment"""
"""Whether the given line matches the definition of a comment"""
return LINE_COMMENT_RE.match(line) is not None
def _match_empty_line(self, line: str) -> bool:
@@ -124,6 +133,14 @@ class SimpleConfigParser:
element["value"].append(line.strip()) # indentation is removed
break
elif self._match_save_config_start(line):
self.current_opt_block = None
self.save_config_block.append(line)
elif self._match_save_config_content(line):
self.current_opt_block = None
self.save_config_block.append(line)
elif self._match_empty_line(line) or self._match_line_comment(line):
self.current_opt_block = None
@@ -185,6 +202,11 @@ class SimpleConfigParser:
if not last_line.endswith("\n"):
f.write("\n")
if self.save_config_block:
for line in self.save_config_block:
f.write(line)
f.write("\n")
def get_sections(self) -> List[str]:
"""Return a list of all section names, but exclude any section starting with '#_'"""
return list(

View File

@@ -0,0 +1,116 @@
# 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 isnt an object and shouldnt 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 %}
#*# <---------------------- SAVE_CONFIG ---------------------->
#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
#*#
#*# [bed_mesh default]
#*# version = 1
#*# points =
#*# -0.152500, -0.133125, -0.113125, -0.159375, -0.232500
#*# -0.095000, -0.078750, -0.068125, -0.133125, -0.235000
#*# -0.092500, -0.040625, 0.004375, -0.077500, -0.193125
#*# -0.073750, 0.023750, 0.085625, 0.026875, -0.085000
#*# -0.140625, 0.038125, 0.126250, 0.097500, 0.003750
#*# tension = 0.2
#*# min_x = 26.0
#*# algo = bicubic
#*# y_count = 5
#*# mesh_y_pps = 2
#*# min_y = 5.0
#*# x_count = 5
#*# max_y = 174.0
#*# mesh_x_pps = 2
#*# max_x = 194.0

View File

@@ -0,0 +1,22 @@
#*# any content
#*#
#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
#*#
#*# [bed_mesh default]
#*# version = 1
#*# points =
#*# -0.152500, -0.133125, -0.113125, -0.159375, -0.232500
#*# -0.095000, -0.078750, -0.068125, -0.133125, -0.235000
#*# -0.092500, -0.040625, 0.004375, -0.077500, -0.193125
#*# -0.073750, 0.023750, 0.085625, 0.026875, -0.085000
#*# -0.140625, 0.038125, 0.126250, 0.097500, 0.003750
#*# tension = 0.2
#*# min_x = 26.0
#*# algo = bicubic
#*# y_count = 5
#*# mesh_y_pps = 2
#*# min_y = 5.0
#*# x_count = 5
#*# max_y = 174.0
#*# mesh_x_pps = 2
#*# max_x = 194.0

View File

@@ -0,0 +1,6 @@
#*# leading space prevents match
random
*# not starting with hash-star-hash
# *# spaced out
<- SAVE_CONFIG ->
;#*# semicolon first

View File

@@ -0,0 +1,37 @@
# ======================================================================= #
# 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.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
def test_matching_lines(parser):
"""Alle Zeilen in matching_data.txt sollen als Save-Config-Content erkannt werden."""
matching_lines = load_testdata_from_file(MATCHING_TEST_DATA_PATH)
for line in matching_lines:
assert parser._match_save_config_content(line) is True, f"Line should be a save config content: {line!r}"
def test_non_matching_lines(parser):
"""Alle Zeilen in non_matching_data.txt sollen NICHT als Save-Config-Content erkannt werden."""
non_matching_lines = load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH)
for line in non_matching_lines:
assert parser._match_save_config_content(line) is False, f"Line should not be a save config content: {line!r}"

View File

@@ -0,0 +1,6 @@
#*# <- SAVE_CONFIG ->
#*# <---- SAVE_CONFIG ---->
#*# <------------------- SAVE_CONFIG ------------------->
#*# <---------------------- SAVE_CONFIG ---------------------->
#*# <----- SAVE_CONFIG ->
#*# <- SAVE_CONFIG ----------------->

View File

@@ -0,0 +1,13 @@
#*#<- SAVE_CONFIG ->
#*# <-SAVE_CONFIG ->
#*# <- SAVE_CONFIG->
#*# <- SAVE_CONFIG -> extra
#*# SAVE_CONFIG ---->
#*# < SAVE_CONFIG >
# *# <- SAVE_CONFIG ->
<- SAVE_CONFIG ->
random text
#*# <---------------------- SAVE_CONFIG ---------------------->
#*# <---------------------- SAVE_CONFIG ----------------------> #*#
#*# <-------------------------------------------->
#*# SAVE_CONFIG

View File

@@ -0,0 +1,37 @@
# ======================================================================= #
# 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.joinpath("test_data")
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
@pytest.fixture
def parser():
return SimpleConfigParser()
def test_matching_lines(parser):
"""Test that all lines in the matching data file are correctly identified as save config start lines."""
matching_lines = load_testdata_from_file(MATCHING_TEST_DATA_PATH)
for line in matching_lines:
assert parser._match_save_config_start(line) is True, f"Line should be a save config start: {line!r}"
def test_non_matching_lines(parser):
"""Test that all lines in the non-matching data file are correctly identified as not save config start lines."""
non_matching_lines = load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH)
for line in non_matching_lines:
assert parser._match_save_config_start(line) is False, f"Line should not be a save config start: {line!r}"

View File

@@ -13,7 +13,7 @@ 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"]
CONFIG_FILES = ["test_config_1.cfg", "test_config_2.cfg", "test_config_3.cfg", "test_config_4.cfg"]
@pytest.fixture(params=CONFIG_FILES)

View File

@@ -9,6 +9,8 @@
from __future__ import annotations
from components.droidklipp.droidklipp import install_droidklipp
import re
from datetime import datetime
from pathlib import Path