Compare commits
520 Commits
v3.0.0
...
kiauh-v6-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d414be609a | ||
|
|
df45c5955e | ||
|
|
70ad635e3d | ||
|
|
6570400f9e | ||
|
|
aafcba9f40 | ||
|
|
91162a7070 | ||
|
|
74c70189af | ||
|
|
017f1d4597 | ||
|
|
0dfe7672b8 | ||
|
|
b3df3e7b5c | ||
|
|
01afe1fe77 | ||
|
|
ac0478b062 | ||
|
|
6eb06772b4 | ||
|
|
d6317ad439 | ||
|
|
e28869be1a | ||
|
|
51993e367d | ||
|
|
a03e943ebf | ||
|
|
fc8fedc9f6 | ||
|
|
7f79f68209 | ||
|
|
a44508ead5 | ||
|
|
9342c94096 | ||
|
|
ea78ba25e6 | ||
|
|
63cae491f3 | ||
|
|
05b5664062 | ||
|
|
a4b149c11a | ||
|
|
3b2bc05746 | ||
|
|
72663ef71c | ||
|
|
8730fc395e | ||
|
|
3885405366 | ||
|
|
e986dfbf4c | ||
|
|
79b4f3eefe | ||
|
|
bf0385e3c9 | ||
|
|
750bf1caaf | ||
|
|
27455dfc64 | ||
|
|
940f7cfbf1 | ||
|
|
e5d0e97b82 | ||
|
|
799892500a | ||
|
|
5f1e42b88b | ||
|
|
09dc961646 | ||
|
|
40e382c9a1 | ||
|
|
9864dd0c7f | ||
|
|
d84adee7f9 | ||
|
|
c17c3e9bd4 | ||
|
|
074344cf7c | ||
|
|
42667ad792 | ||
|
|
9804411d74 | ||
|
|
067a102b6b | ||
|
|
4a5d1a971a | ||
|
|
6407664e3e | ||
|
|
65617ca971 | ||
|
|
e05a42630e | ||
|
|
be228210bd | ||
|
|
b70ac0dfd7 | ||
|
|
af48738221 | ||
|
|
9d2cb72aa4 | ||
|
|
8c3397ea78 | ||
|
|
7d3d46ac07 | ||
|
|
3da7aedd7f | ||
|
|
8d343853f1 | ||
|
|
1f2d724189 | ||
|
|
1a29324e6a | ||
|
|
5225e70e83 | ||
|
|
51f0713c5a | ||
|
|
d420daca26 | ||
|
|
cb62909f41 | ||
|
|
02eebff571 | ||
|
|
36b295bd1b | ||
|
|
372c9c0b7d | ||
|
|
c67ea2245d | ||
|
|
fda99bb70a | ||
|
|
2c1c94c904 | ||
|
|
b020f10967 | ||
|
|
aa1b435da5 | ||
|
|
449317b118 | ||
|
|
336414c43c | ||
|
|
cd63034b74 | ||
|
|
8de7ab7e11 | ||
|
|
c2b0ca5b19 | ||
|
|
ecb673a088 | ||
|
|
da4c5fe109 | ||
|
|
bb769fdf6d | ||
|
|
409aa3da25 | ||
|
|
0b41d63496 | ||
|
|
44301c0c87 | ||
|
|
ace47e2873 | ||
|
|
06801a47eb | ||
|
|
1484ebf445 | ||
|
|
4547ac571a | ||
|
|
b2dd5d8ed7 | ||
|
|
e50ce1fc71 | ||
|
|
417180f724 | ||
|
|
f2691f33d3 | ||
|
|
39f0bd8b0a | ||
|
|
dc87d30770 | ||
|
|
aaf5216275 | ||
|
|
ebdfadac07 | ||
|
|
cac73cc58d | ||
|
|
78dbf31576 | ||
|
|
fef8b58510 | ||
|
|
d800d356ca | ||
|
|
72e3a56e4f | ||
|
|
e64aa94df4 | ||
|
|
58719a4ca0 | ||
|
|
59a83aee12 | ||
|
|
7104eb078f | ||
|
|
341ecb325c | ||
|
|
e3a6d8a0ab | ||
|
|
0183988d5d | ||
|
|
03c3ed20f3 | ||
|
|
5c1c98b6b8 | ||
|
|
ef13c130e0 | ||
|
|
2acd74cbd9 | ||
|
|
00665109c2 | ||
|
|
a5dce136f3 | ||
|
|
4ffa057931 | ||
|
|
ed35dc9e03 | ||
|
|
7ec055f562 | ||
|
|
9eb0531cdf | ||
|
|
84cda99af8 | ||
|
|
5f823c2d3a | ||
|
|
758a783ede | ||
|
|
682baaa105 | ||
|
|
601ccb2191 | ||
|
|
c0caab13b3 | ||
|
|
7c754de08e | ||
|
|
9dc556e7e4 | ||
|
|
655b781aef | ||
|
|
14aafd558a | ||
|
|
bd1aa1ae2b | ||
|
|
8df75dc8d0 | ||
|
|
5c37b68463 | ||
|
|
1620efe56c | ||
|
|
7fd91e6cef | ||
|
|
750cb7b307 | ||
|
|
384503c4f5 | ||
|
|
b6c6edb622 | ||
|
|
2a4fcf3a3a | ||
|
|
573dc7c3c9 | ||
|
|
05b4ef2d18 | ||
|
|
863c62511c | ||
|
|
be5f345a7c | ||
|
|
948927cfd3 | ||
|
|
34ebe5d15e | ||
|
|
3bef6ecb85 | ||
|
|
5ace920d3e | ||
|
|
2f34253bad | ||
|
|
0447bc4405 | ||
|
|
7cb2231584 | ||
|
|
5a3d21c40b | ||
|
|
099d47df2f | ||
|
|
ba1cdb3739 | ||
|
|
ad56b51e70 | ||
|
|
c6999f1990 | ||
|
|
bc30cf418b | ||
|
|
ee81ee4c0c | ||
|
|
35911604af | ||
|
|
77f1089041 | ||
|
|
8e7d4db988 | ||
|
|
8f960495ba | ||
|
|
095823bf28 | ||
|
|
397038e43e | ||
|
|
061e222664 | ||
|
|
3f5ff50d69 | ||
|
|
7820155094 | ||
|
|
c28d5c28b9 | ||
|
|
cda6d31a7c | ||
|
|
9a657daffd | ||
|
|
85b4b68f16 | ||
|
|
dfbce3b489 | ||
|
|
f3b0e45e39 | ||
|
|
83e5d9c0d5 | ||
|
|
8f44187568 | ||
|
|
625a808484 | ||
|
|
ad0dbf63b8 | ||
|
|
9dedf38079 | ||
|
|
1b4c76d080 | ||
|
|
d20d82aeac | ||
|
|
16a28ffda0 | ||
|
|
a9367cc064 | ||
|
|
b165d88855 | ||
|
|
6c59d58193 | ||
|
|
b4f5c3c1ac | ||
|
|
b69ecbc9b5 | ||
|
|
fc9fa39eee | ||
|
|
142b4498a3 | ||
|
|
012b6c4bb7 | ||
|
|
8aeb01aca0 | ||
|
|
da533fdd67 | ||
|
|
8cb0754296 | ||
|
|
7a6590e86a | ||
|
|
2f0feb317e | ||
|
|
b9479db766 | ||
|
|
14132fc34b | ||
|
|
3d5e83d5ab | ||
|
|
edd5f5c6fd | ||
|
|
8ff0b9d81d | ||
|
|
22e8e314db | ||
|
|
12bd8eb799 | ||
|
|
4915896099 | ||
|
|
cd38970bbd | ||
|
|
b8640f45a6 | ||
|
|
5fb4444f03 | ||
|
|
926ba1acb4 | ||
|
|
c2e7ee98df | ||
|
|
3865266da1 | ||
|
|
b83f642a13 | ||
|
|
30b4414469 | ||
|
|
1178d3c730 | ||
|
|
59d8867c8c | ||
|
|
80a953a587 | ||
|
|
a80f0bb0e8 | ||
|
|
78cefddb2e | ||
|
|
b20613819e | ||
|
|
5ebe941125 | ||
|
|
f5eb9486cc | ||
|
|
1836beab42 | ||
|
|
545397f598 | ||
|
|
f709cf84e7 | ||
|
|
f62c10dc8b | ||
|
|
7a9e752f9c | ||
|
|
30bc56b198 | ||
|
|
b2567995de | ||
|
|
e121ba8a62 | ||
|
|
9a1a66aa64 | ||
|
|
420b193f4b | ||
|
|
de20f0c412 | ||
|
|
57f34b07c6 | ||
|
|
e35e44a76a | ||
|
|
bfb10c742b | ||
|
|
458c89a78a | ||
|
|
6128e35d45 | ||
|
|
279d000bb0 | ||
|
|
a4a3d5eecb | ||
|
|
1392ca9f82 | ||
|
|
47121f6875 | ||
|
|
d0d2404132 | ||
|
|
6ed5395f17 | ||
|
|
be805c169b | ||
|
|
eaf12db27e | ||
|
|
fe8767113b | ||
|
|
2148d95cf4 | ||
|
|
682be48e8d | ||
|
|
68369753fd | ||
|
|
44ed3b6ddf | ||
|
|
e12e578098 | ||
|
|
515a42f098 | ||
|
|
f9ecad0eca | ||
|
|
fb09acf660 | ||
|
|
093da73dd1 | ||
|
|
c9e8c4807e | ||
|
|
6fcd7a3f08 | ||
|
|
09e874214b | ||
|
|
623bd7553b | ||
|
|
1e0c74b549 | ||
|
|
358c666da9 | ||
|
|
84a530be7d | ||
|
|
bfff3019cb | ||
|
|
2a100c2934 | ||
|
|
25dfbb83df | ||
|
|
ce0daa52ae | ||
|
|
899b204dc7 | ||
|
|
5cf4b018fc | ||
|
|
ae9d1b98da | ||
|
|
16d3388ff2 | ||
|
|
b88d0085ba | ||
|
|
0b6613e464 | ||
|
|
d99cda544a | ||
|
|
a50dce20de | ||
|
|
f45da66e9e | ||
|
|
2822499344 | ||
|
|
c777ba3e6b | ||
|
|
9f410450d7 | ||
|
|
0497d49066 | ||
|
|
229da227b0 | ||
|
|
65854c8da6 | ||
|
|
5985646633 | ||
|
|
979c39dc02 | ||
|
|
197058bd00 | ||
|
|
d3b5122ebb | ||
|
|
8ce4daf403 | ||
|
|
b0a65fe14e | ||
|
|
98866caefa | ||
|
|
345b7b66a3 | ||
|
|
8eb2924832 | ||
|
|
5d7debd65e | ||
|
|
7df3dd489f | ||
|
|
0cd058320f | ||
|
|
bcbb185bd7 | ||
|
|
477f3ca72c | ||
|
|
c19acb1694 | ||
|
|
8228943850 | ||
|
|
5b890fb0fb | ||
|
|
7989cec8d4 | ||
|
|
858301aa9a | ||
|
|
ae9e79c579 | ||
|
|
1215446a6c | ||
|
|
8526acf8b6 | ||
|
|
cc27aaec7c | ||
|
|
1e9493461c | ||
|
|
31616ebad5 | ||
|
|
faf56ed1b1 | ||
|
|
d6837af2a2 | ||
|
|
afe6f7499a | ||
|
|
e3ed223b5c | ||
|
|
fd27db28d4 | ||
|
|
68a02ad3f5 | ||
|
|
99b7672dc9 | ||
|
|
bb3ec79756 | ||
|
|
ce595abd60 | ||
|
|
c79dc280e3 | ||
|
|
7aa186e8b9 | ||
|
|
8493269c6f | ||
|
|
150ef0142f | ||
|
|
f70faa52cc | ||
|
|
e796f74640 | ||
|
|
2c9f5bed60 | ||
|
|
e9c23ca93e | ||
|
|
67afa26ed7 | ||
|
|
54be7e4e21 | ||
|
|
811c071b74 | ||
|
|
6116fc92cf | ||
|
|
5524a40f04 | ||
|
|
cb3661b8b5 | ||
|
|
2cec90b29c | ||
|
|
d2c009df9a | ||
|
|
046178f801 | ||
|
|
442980dbd0 | ||
|
|
798e56f4dc | ||
|
|
9d90daec7f | ||
|
|
f25726cfed | ||
|
|
f46b099b74 | ||
|
|
03be46f012 | ||
|
|
c11e628c55 | ||
|
|
4c8d43e365 | ||
|
|
9d7144b493 | ||
|
|
6df388f42b | ||
|
|
1d7fb010af | ||
|
|
d4207d710c | ||
|
|
6cb8d70b63 | ||
|
|
ae011963da | ||
|
|
491d6f40bb | ||
|
|
8bbe2f79ea | ||
|
|
0bdf61a714 | ||
|
|
b07a83c8ad | ||
|
|
39e22acbed | ||
|
|
8ba46fa4ac | ||
|
|
d6b95c9d10 | ||
|
|
f3a769e03e | ||
|
|
646e5acd3a | ||
|
|
fcf059df73 | ||
|
|
4bf9e8f0a8 | ||
|
|
dd58229fee | ||
|
|
6c4635fa4e | ||
|
|
4517415e9d | ||
|
|
5c45bc7617 | ||
|
|
d8ce465126 | ||
|
|
2f8c95a8c7 | ||
|
|
4c083ceade | ||
|
|
259a6919f0 | ||
|
|
4f7a49d85a | ||
|
|
005a5061a7 | ||
|
|
634d795557 | ||
|
|
a14e321df9 | ||
|
|
1682642e47 | ||
|
|
7e9d18b54c | ||
|
|
d049d4c770 | ||
|
|
108cda3cd6 | ||
|
|
d7de58f538 | ||
|
|
572afa0396 | ||
|
|
63a5e1e323 | ||
|
|
8c68eaa995 | ||
|
|
e8c0b3cf39 | ||
|
|
cfad7a1fb0 | ||
|
|
4113732daa | ||
|
|
95808a0d5b | ||
|
|
e551c02507 | ||
|
|
1f40686ea1 | ||
|
|
9b3d96545b | ||
|
|
a632fae8f6 | ||
|
|
4e3a701db4 | ||
|
|
0c760b5aa2 | ||
|
|
3bc2f3b498 | ||
|
|
b92cfc3984 | ||
|
|
01790b5c11 | ||
|
|
8c7891e360 | ||
|
|
852f7c056a | ||
|
|
8cffd07aef | ||
|
|
40745e90df | ||
|
|
a43645cca0 | ||
|
|
4d834db5df | ||
|
|
771191ab69 | ||
|
|
7afe943ecc | ||
|
|
b06c17c184 | ||
|
|
3af46b45ee | ||
|
|
b58e79634c | ||
|
|
1ef9b0f58f | ||
|
|
8333ae1dc4 | ||
|
|
e0ae312a9e | ||
|
|
4ce1ce72d3 | ||
|
|
60842a330d | ||
|
|
05a59e9261 | ||
|
|
36a8757cfd | ||
|
|
fe4625d3e1 | ||
|
|
19ddf3e023 | ||
|
|
ba888b1f97 | ||
|
|
0284a36e7f | ||
|
|
22f705e06c | ||
|
|
4c34245da0 | ||
|
|
f7cb3d6c97 | ||
|
|
aaf4f7dd5c | ||
|
|
26bac791aa | ||
|
|
aa4bdfc7b2 | ||
|
|
311f3be864 | ||
|
|
511df1a889 | ||
|
|
8d3ddc273a | ||
|
|
f231fa9c69 | ||
|
|
9b6925e9c4 | ||
|
|
7f8ee7939c | ||
|
|
4d4c49d4c9 | ||
|
|
7692227946 | ||
|
|
3da993a67c | ||
|
|
6b74c59d15 | ||
|
|
9cd27f7052 | ||
|
|
fc4fe130cd | ||
|
|
2a46b00cda | ||
|
|
560186a40b | ||
|
|
75bca847f8 | ||
|
|
bb1f2eadca | ||
|
|
6d87716b1d | ||
|
|
1e8c379623 | ||
|
|
6a8991d51e | ||
|
|
fb4367bb41 | ||
|
|
9463b719e4 | ||
|
|
65bf3d5251 | ||
|
|
68327262fc | ||
|
|
14ef39b87c | ||
|
|
969d3b5dab | ||
|
|
05842f8e1d | ||
|
|
39219c105e | ||
|
|
7984c28fe5 | ||
|
|
b44e855a98 | ||
|
|
52ab909ba5 | ||
|
|
4d7e10e5c3 | ||
|
|
d3726733e5 | ||
|
|
765f016ea2 | ||
|
|
5deb987b8a | ||
|
|
47d1321979 | ||
|
|
b47a9cf7ed | ||
|
|
a9f23e9b23 | ||
|
|
814acbe92a | ||
|
|
991dd79d01 | ||
|
|
40875dfe49 | ||
|
|
806c6fd275 | ||
|
|
e9706b52d8 | ||
|
|
412e084d6d | ||
|
|
3ebee823ad | ||
|
|
72312422e3 | ||
|
|
54089582e4 | ||
|
|
9fd3f930df | ||
|
|
98f0aa4b8f | ||
|
|
19b37e4dc4 | ||
|
|
d344b1c5f6 | ||
|
|
f804fcb65d | ||
|
|
0edfc746d4 | ||
|
|
02ef0578e3 | ||
|
|
5d11cd212a | ||
|
|
bf33c77db7 | ||
|
|
0815d7778c | ||
|
|
9a3814f480 | ||
|
|
a83585fb06 | ||
|
|
dc27fe47e1 | ||
|
|
11b3d7a961 | ||
|
|
9e0cdb0715 | ||
|
|
8abda56749 | ||
|
|
cf20fc3c48 | ||
|
|
6c8845d7b4 | ||
|
|
fb0a30814d | ||
|
|
87f229c62d | ||
|
|
5b1da45688 | ||
|
|
545b978e80 | ||
|
|
44f5609de6 | ||
|
|
f2a26d9b3d | ||
|
|
bb2cdab02b | ||
|
|
d6596d0a3d | ||
|
|
865fbc07dc | ||
|
|
40ba33eb19 | ||
|
|
49b77162b0 | ||
|
|
adf087e3e5 | ||
|
|
8220647564 | ||
|
|
57075ff525 | ||
|
|
2f9de620bc | ||
|
|
d658c3a4cd | ||
|
|
67f6a2c599 | ||
|
|
f369c132d2 | ||
|
|
cce6ac3c88 | ||
|
|
b9fe29068d | ||
|
|
104089ea3d | ||
|
|
48d97dab01 | ||
|
|
e4a56564a1 | ||
|
|
6de8ce3278 | ||
|
|
b46457328f | ||
|
|
7c7dd4ec3c | ||
|
|
749293dde0 | ||
|
|
85f8ab4f24 | ||
|
|
e894496a6e | ||
|
|
19b220b772 | ||
|
|
a4eaa8952a | ||
|
|
74a9bf783b | ||
|
|
7597e999a1 | ||
|
|
64d73ca86a | ||
|
|
d3283f7ab7 | ||
|
|
cb443f6fee | ||
|
|
7162256e3f | ||
|
|
938b9d24f5 | ||
|
|
38a3517c89 | ||
|
|
0b15cd0d9b | ||
|
|
9d7752a6c5 | ||
|
|
d1dff4d3b5 | ||
|
|
49b7fe395b | ||
|
|
51469d8992 |
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
max_line_length = 88
|
||||||
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: dw__0
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: https://paypal.me/dwillner0
|
||||||
50
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
50
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for reporting bugs only!
|
||||||
|
If you have a feature request, please use [feature_request](/new?template=feature_request.yml)
|
||||||
|
- type: textarea
|
||||||
|
id: distro
|
||||||
|
attributes:
|
||||||
|
label: Linux Distribution
|
||||||
|
description: >-
|
||||||
|
The linux distribution the issue occured on
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened
|
||||||
|
description: >-
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: What did you expect to happen
|
||||||
|
description: >-
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: repro-steps
|
||||||
|
attributes:
|
||||||
|
label: How to reproduce
|
||||||
|
description: >-
|
||||||
|
Minimal and precise steps to reproduce this bug.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional-info
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |
|
||||||
|
If you have any additional information for us, use the field below.
|
||||||
|
|
||||||
|
Please note, you can attach screenshots or screen recordings here, by
|
||||||
|
dragging and dropping files in the field below.
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Klipper Discord
|
||||||
|
url: https://discord.klipper3d.org/
|
||||||
|
about: Quickest way to get in contact
|
||||||
40
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
40
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: ["feature request"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for feature requests only!
|
||||||
|
If you've found a bug, please use [bug_report](/new?template=bug_report.yml)
|
||||||
|
- type: textarea
|
||||||
|
id: problem-description
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem? Please describe
|
||||||
|
description: >-
|
||||||
|
A clear and concise description of what the problem is.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solution-description
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: >-
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: possible-alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
description: >-
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
- type: textarea
|
||||||
|
id: additional-info
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |
|
||||||
|
If you have any additional information for us, use the field below.
|
||||||
|
|
||||||
|
Please note, you can attach screenshots or screen recordings here, by
|
||||||
|
dragging and dropping files in the field below.
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1 +1,8 @@
|
|||||||
kiauh.ini
|
.idea
|
||||||
|
.vscode
|
||||||
|
.pytest_cache
|
||||||
|
__pycache__
|
||||||
|
.kiauh-env
|
||||||
|
*.code-workspace
|
||||||
|
*.iml
|
||||||
|
kiauh.cfg
|
||||||
|
|||||||
15
.shellcheckrc
Normal file
15
.shellcheckrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
source=scripts
|
||||||
|
|
||||||
|
enable=avoid-nullary-conditions
|
||||||
|
enable=deprecate-which
|
||||||
|
enable=quote-safe-variables
|
||||||
|
enable=require-variable-braces
|
||||||
|
enable=require-double-brackets
|
||||||
|
|
||||||
|
# SC2162: `read` without `-r` will mangle backslashes.
|
||||||
|
# https://github.com/koalaman/shellcheck/wiki/SC2162
|
||||||
|
disable=SC2162
|
||||||
|
|
||||||
|
# SC2164: Use `cd ... || exit` in case `cd` fails
|
||||||
|
# https://github.com/koalaman/shellcheck/wiki/SC2164
|
||||||
|
disable=SC2164
|
||||||
272
README.md
272
README.md
@@ -1,146 +1,202 @@
|
|||||||
# **KIAUH - Klipper Installation And Update Helper**
|
<p align="center">
|
||||||
|
<a>
|
||||||
|
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/kiauh.png" alt="KIAUH logo" height="181">
|
||||||
|
<h1 align="center">Klipper Installation And Update Helper</h1>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||

|
<p align="center">
|
||||||
|
A handy installation script that makes installing Klipper (and more) a breeze!
|
||||||
|
</p>
|
||||||
|
|
||||||
---
|
<p align="center">
|
||||||
|
<a><img src="https://img.shields.io/github/license/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/stars/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/forks/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/languages/top/dw-0/kiauh?logo=gnubash&logoColor=white"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/v/tag/dw-0/kiauh"></a>
|
||||||
|
<br />
|
||||||
|
<a><img src="https://img.shields.io/github/last-commit/dw-0/kiauh"></a>
|
||||||
|
<a><img src="https://img.shields.io/github/contributors/dw-0/kiauh"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## **📋 Please see the [Changelog](docs/changelog.md) for possible important information !**
|
<hr>
|
||||||
|
|
||||||
---
|
<h2 align="center">
|
||||||
|
📄️ Instructions 📄
|
||||||
|
</h2>
|
||||||
|
|
||||||
## **📢 Disclaimer: Usage of this script happens at your own risk!**
|
### 📋 Prerequisites
|
||||||
|
KIAUH is a script that assists you in installing Klipper on a Linux operating system that has
|
||||||
|
already been flashed to your Raspberry Pi's (or other SBC's) SD card. As a result, you must ensure
|
||||||
|
that you have a functional Linux system on hand. `Raspberry Pi OS Lite (either 32bit or 64bit)` is a recommended Linux image
|
||||||
|
if you are using a Raspberry Pi. The [official Raspberry Pi Imager](https://www.raspberrypi.com/software/)
|
||||||
|
is the simplest way to flash an image like this to an SD card.
|
||||||
|
|
||||||
This script acts as a helping hand for you to get set up in a fast and comfortable way.\
|
* Once you have downloaded, installed and launched the Raspberry Pi Imager,
|
||||||
**This does not mean, it will relieve you of using your brain.exe! 🧠**\
|
select `Choose OS -> Raspberry Pi OS (other)`: \
|
||||||
Please also always pay attention to the individual component repositories (all linked below).\
|
<p align="center">
|
||||||
Feel free to give it a try. If you have suggestions or encounter any problems, please report them.
|
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager1.png" alt="KIAUH logo" height="350">
|
||||||
|
</p>
|
||||||
|
|
||||||
---
|
* Then select `Raspberry Pi OS Lite (32bit)` (or 64bit if you want to use that instead):
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/dw-0/kiauh/master/resources/screenshots/rpi_imager2.png" alt="KIAUH logo" height="350">
|
||||||
|
</p>
|
||||||
|
|
||||||
## **🛠️ Instructions:**
|
* Back in the Raspberry Pi Imager's main menu, select the corresponding SD card to which
|
||||||
|
you want to flash the image.
|
||||||
|
|
||||||
For downloading this script it is necessary to have git installed.\
|
* Make sure to go into the Advanced Option (the cog icon in the lower left corner of the main menu)
|
||||||
If you haven't, please run `sudo apt-get install git -y` to install git first.\
|
and enable SSH and configure Wi-Fi.
|
||||||
You will need it anyways! 😄
|
|
||||||
|
|
||||||
After git is installed, use the following commands in the given order to download and execute the script:
|
* If you need more help for using the Raspberry Pi Imager, please visit the [official documentation](https://www.raspberrypi.com/documentation/computers/getting-started.html).
|
||||||
|
|
||||||
|
These steps **only** apply if you are actually using a Raspberry Pi. In case you want
|
||||||
|
to use a different SBC (like an Orange Pi or any other Pi derivates), please look up on how to get an appropriate Linux image flashed
|
||||||
|
to the SD card before proceeding further (usually done with Balena Etcher in those cases). Also make sure that KIAUH will be able to run
|
||||||
|
and operate on the Linux Distribution you are going to flash. You likely will have the most success with
|
||||||
|
distributions based on Debian 11 Bullseye. Read the notes further down below in this document.
|
||||||
|
|
||||||
|
### 💾 Download and use KIAUH
|
||||||
|
**📢 Disclaimer: Usage of this script happens at your own risk!**
|
||||||
|
|
||||||
|
* **Step 1:** \
|
||||||
|
To download this script, it is necessary to have git installed. If you don't have git already installed, or if you are unsure, run the following command:
|
||||||
|
```shell
|
||||||
|
sudo apt-get update && sudo apt-get install git -y
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Step 2:** \
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Step 3:** \
|
||||||
|
Finally, start KIAUH by running the next command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd ~
|
|
||||||
git clone https://github.com/th33xitus/kiauh.git
|
|
||||||
./kiauh/kiauh.sh
|
./kiauh/kiauh.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Additional Instructions:
|
* **Step 4:** \
|
||||||
|
You should now find yourself in the main menu of KIAUH. You will see several actions to choose from depending
|
||||||
|
on what you want to do. To choose an action, simply type the corresponding number into the "Perform action"
|
||||||
|
prompt and confirm by hitting ENTER.
|
||||||
|
|
||||||
If you need some more detailed instructions on how to install Klipper and Mainsail with KIAUH, check out this website:\
|
<hr>
|
||||||
[Installing Klipper and Mainsail](https://3dp.tumbleweedlabs.com/firmware/klipper-firmware/installing-klipper-and-mainsail-on-your-raspberry-pi)\
|
|
||||||
Credits for these instructions go to [@tumbleweedlabs](https://github.com/tumbleweedlabs).\
|
|
||||||
Feel free to check out his work.
|
|
||||||
|
|
||||||
---
|
<h2 align="center">❗ Notes ❗</h2>
|
||||||
|
|
||||||
## **🧰 Functions and Features:**
|
### **📋 Please see the [Changelog](docs/changelog.md) for possible important changes!**
|
||||||
|
|
||||||
- **New in v3.0** You can now install multiple instances (Klipper/Moonraker/DWC/Octoprint) on the same Pi!
|
- Mainly tested on Raspberry Pi OS Lite (Debian 10 Buster / Debian 11 Bullseye)
|
||||||
---
|
- Other Debian based distributions (like Ubuntu 20 to 22) likely work too
|
||||||
### **Core Functions:**
|
- Reported to work on Armbian as well but not tested in detail
|
||||||
|
- During the use of this script you will be asked for your sudo password. There are several functions involved which need sudo privileges.
|
||||||
|
|
||||||
- **Installing** Klipper to your Raspberry Pi or other Debian based Linux Distribution.
|
<hr>
|
||||||
- **Installing** of the Moonraker API (needed for Mainsail, Fluidd and KlipperScreen)
|
|
||||||
- **Installing** several different web interfaces such as Mainsail, Fluidd, Duet Web Control or OctoPrint including their dependencies.
|
|
||||||
- **Installing** of KlipperScreen (OctoScreen but for Klipper!)
|
|
||||||
- **Updating** of all the listed installations above excluding OctoPrint. For updating OctoPrint, please use the OctoPrint interface!
|
|
||||||
- **Removing** of all the listed installations above.
|
|
||||||
- **Backup** of all the listed installations above.
|
|
||||||
|
|
||||||
### **Also possible:**
|
<h2 align="center">🌐 Sources & Further Information</h2>
|
||||||
|
|
||||||
- Build the Klipper Firmware
|
<table>
|
||||||
- Flash the MCU
|
<tr>
|
||||||
- Read ID of the currently connected MCU
|
<th><h3><a href="https://github.com/Klipper3d/klipper">Klipper</a></h3></th>
|
||||||
- and more ...
|
<th><h3><a href="https://github.com/Arksine/moonraker">Moonraker</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/mainsail-crew/mainsail">Mainsail</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="https://raw.githubusercontent.com/Klipper3d/klipper/master/docs/img/klipper-logo.png" alt="Klipper Logo" height="64"></th>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/9563098?v=4" alt="Arksine avatar" height="64"></th>
|
||||||
|
<th><img src="https://raw.githubusercontent.com/mainsail-crew/docs/master/assets/img/logo.png" alt="Mainsail Logo" height="64"></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>by <a href="https://github.com/KevinOConnor">KevinOConnor</a></th>
|
||||||
|
<th>by <a href="https://github.com/Arksine">Arksine</a></th>
|
||||||
|
<th>by <a href="https://github.com/mainsail-crew">mainsail-crew</a></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><h3><a href="https://github.com/fluidd-core/fluidd">Fluidd</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/jordanruthe/KlipperScreen">KlipperScreen</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><img src="https://raw.githubusercontent.com/fluidd-core/fluidd/master/docs/assets/images/logo.svg" alt="Fluidd Logo" height="64"></th>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/31575189?v=4" alt="jordanruthe avatar" height="64"></th>
|
||||||
|
<th><img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint/master/docs/images/octoprint-logo.png" alt="OctoPrint Logo" height="64"></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>by <a href="https://github.com/fluidd-core">fluidd-core</a></th>
|
||||||
|
<th>by <a href="https://github.com/jordanruthe">jordanruthe</a></th>
|
||||||
|
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
### **For a list of additional features please see: [Feature List](docs/features.md)**
|
<tr>
|
||||||
|
<th><h3><a href="https://github.com/nlef/moonraker-telegram-bot">Moonraker-Telegram-Bot</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/Kragrathea/pgcode">PrettyGCode for Klipper</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/TheSpaghettiDetective/moonraker-obico">Obico for Klipper</a></h3></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
---
|
<tr>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/52351624?v=4" alt="nlef avatar" height="64"></th>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/5917231?v=4" alt="Kragrathea avatar" height="64"></th>
|
||||||
|
<th><img src="https://avatars.githubusercontent.com/u/46323662?s=200&v=4" alt="Obico logo" height="64"></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
## **📝 Notes:**
|
<tr>
|
||||||
|
<th>by <a href="https://github.com/nlef">nlef</a></th>
|
||||||
|
<th>by <a href="https://github.com/Kragrathea">Kragrathea</a></th>
|
||||||
|
<th>by <a href="https://github.com/TheSpaghettiDetective">Obico</a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
- Tested **only** on Raspberry Pi OS Lite (Debian Buster)
|
<tr>
|
||||||
- ( Although similar Debian based distributions might also work... )
|
<th><h3><a href="https://github.com/Clon1998/mobileraker_companion">Mobileraker's Companion</a></h3></th>
|
||||||
- During the use of this script you might be asked for your sudo password. There are several functions involved which need sudo privileges.
|
<th><h3><a href="https://octoeverywhere.com/?source=kiauh_readme">OctoEverywhere For Klipper</a></h3></th>
|
||||||
|
<th><h3><a href="https://github.com/crysxd/OctoPrint-OctoApp">OctoApp For Klipper</a></h3></th>
|
||||||
|
<th><h3></h3></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
---
|
<tr>
|
||||||
|
<th><a href="https://github.com/Clon1998/mobileraker_companion"><img src="https://raw.githubusercontent.com/Clon1998/mobileraker/master/assets/icon/mr_appicon.png" alt="OctoEverywhere Logo" height="64"></a></th>
|
||||||
|
<th><a href="https://octoeverywhere.com/?source=kiauh_readme"><img src="https://octoeverywhere.com/img/logo.svg" alt="OctoEverywhere Logo" height="64"></a></th>
|
||||||
|
<th><a href="https://octoapp.eu/?source=kiauh_readme"><img src="https://octoapp.eu/octoapp.webp" alt="OctoApp Logo" height="64"></a></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
## **🛈 Sources & Further Information**
|
<tr>
|
||||||
|
<th>by <a href="https://github.com/Clon1998">Patrick Schmidt</a></th>
|
||||||
|
<th>by <a href="https://github.com/QuinnDamerell">Quinn Damerell</a></th>
|
||||||
|
<th>by <a href="https://github.com/crysxd">Christian Würthner</a></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
If you need some more detailed instructions on how to install Klipper and Mainsail with KIAUH, check out this website:\
|
|
||||||
[Installing Klipper and Mainsail](https://3dp.tumbleweedlabs.com/firmware/klipper-firmware/installing-klipper-and-mainsail-on-your-raspberry-pi)\
|
|
||||||
Credits for these instructions go to [@tumbleweedlabs](https://github.com/tumbleweedlabs).
|
|
||||||
|
|
||||||
For more information or instructions to the various components KIAUH can install, please check out the corresponding repositories listed below:
|
</table>
|
||||||
|
|
||||||
---
|
<hr>
|
||||||
|
|
||||||
### **⛵Klipper** by [KevinOConnor](https://github.com/KevinOConnor) :
|
<h2 align="center">🎖️ Contributors 🎖️</h2>
|
||||||
|
|
||||||
https://github.com/KevinOConnor/klipper
|
<div align="center">
|
||||||
|
<a href="https://github.com/dw-0/kiauh/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=dw-0/kiauh" alt=""/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
---
|
<hr>
|
||||||
|
|
||||||
### **⛵Klipper S-Curve fork** by [dmbutyugin](https://github.com/dmbutyugin) :
|
<h2 align="center">✨ Credits ✨</h2>
|
||||||
|
|
||||||
https://github.com/dmbutyugin/klipper/tree/scurve-smoothing \
|
* A big thank you to [lixxbox](https://github.com/lixxbox) for that awesome KIAUH-Logo!
|
||||||
https://github.com/dmbutyugin/klipper/tree/scurve-shaping
|
* Also, a big thank you to everyone who supported my work with a [Ko-fi](https://ko-fi.com/dw__0) !
|
||||||
|
* Last but not least: Thank you to all contributors and members of the Klipper Community who like and share this project!
|
||||||
|
|
||||||
---
|
<hr>
|
||||||
|
|
||||||
### **🌙Moonraker** by [Arksine](https://github.com/Arksine) :
|
<h4 align="center">A special thank you to JetBrains for sponsoring this project with their incredible software!</h4>
|
||||||
|
<p align="center">
|
||||||
https://github.com/Arksine/moonraker
|
<a href="https://www.jetbrains.com/community/opensource/#support" target="_blank">
|
||||||
|
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo." height="128">
|
||||||
---
|
</a>
|
||||||
|
</p>
|
||||||
### **💨Mainsail Webinterface** by [meteyou](https://github.com/meteyou) :
|
|
||||||
|
|
||||||
https://github.com/meteyou/mainsail
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **🌊Fluidd Webinterface** by [cadriel](https://github.com/cadriel) :
|
|
||||||
|
|
||||||
https://github.com/cadriel/fluidd
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **🕸️Duet Web Control** by [Duet3D](https://github.com/Duet3D) :
|
|
||||||
|
|
||||||
https://github.com/Duet3D/DuetWebControl
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **🕸️DWC2-for-Klipper-Socket** by [Stephan3](https://github.com/Stephan3) :
|
|
||||||
|
|
||||||
https://github.com/Stephan3/dwc2-for-klipper-socket
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **🖥️KlipperScreen** by [jordanruthe](https://github.com/jordanruthe) :
|
|
||||||
|
|
||||||
https://github.com/jordanruthe/KlipperScreen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **🐙OctoPrint Webinterface** by [OctoPrint](https://github.com/OctoPrint) :
|
|
||||||
|
|
||||||
https://octoprint.org \
|
|
||||||
https://github.com/OctoPrint/OctoPrint
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **❓ FAQ**
|
|
||||||
|
|
||||||
**_Q: Can i use this script to install multiple instances of Klipper on the same Pi? (Multisession?)_**
|
|
||||||
|
|
||||||
**A:** Yes, it is finally possible 🙂
|
|
||||||
|
|||||||
18
default_kiauh.cfg
Normal file
18
default_kiauh.cfg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[kiauh]
|
||||||
|
backup_before_update: False
|
||||||
|
|
||||||
|
[klipper]
|
||||||
|
repo_url: https://github.com/Klipper3d/klipper
|
||||||
|
branch: master
|
||||||
|
|
||||||
|
[moonraker]
|
||||||
|
repo_url: https://github.com/Arksine/moonraker
|
||||||
|
branch: master
|
||||||
|
|
||||||
|
[mainsail]
|
||||||
|
port: 80
|
||||||
|
unstable_releases: False
|
||||||
|
|
||||||
|
[fluidd]
|
||||||
|
port: 81
|
||||||
|
unstable_releases: False
|
||||||
@@ -2,6 +2,173 @@
|
|||||||
|
|
||||||
This document covers possible important changes to KIAUH.
|
This document covers possible important changes to KIAUH.
|
||||||
|
|
||||||
|
### 2023-06-17
|
||||||
|
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
|
||||||
|
to control your printer. Thank you to [Clon1998](https://github.com/Clon1998) for adding this feature!
|
||||||
|
|
||||||
|
### 2023-02-03
|
||||||
|
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 \
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 2022-10-31
|
||||||
|
Some functions got updated, though not all of them.
|
||||||
|
|
||||||
|
The following functions are still currently unavailable:
|
||||||
|
- Installation of: MJPG-Streamer
|
||||||
|
- All backup functions and the Log-Upload
|
||||||
|
|
||||||
|
### 2022-10-20
|
||||||
|
KIAUH has now reached major version 5 !
|
||||||
|
|
||||||
|
Recently Moonraker introduced some changes which makes it necessary to change the folder structure of printer setups.
|
||||||
|
If you are interested in the details, check out this PR: https://github.com/Arksine/moonraker/pull/491 \
|
||||||
|
Although Moonraker has some mechanics available to migrate existing setups to the new file structure with the use of symlinks, fresh and clean installs
|
||||||
|
should be considered.
|
||||||
|
|
||||||
|
The version jump of KIAUH to v5 is a breaking change due to those major changes! That means v4 and v5 are not compatible with each other!
|
||||||
|
This is also the reason why you will currently be greeted by a yellow notification in the main menu of KIAUH leading to this changelog.
|
||||||
|
I decided to disable a few functions of the script and focus on releasing the required changes to the core components of this script.
|
||||||
|
I will work on updating the other parts of the script piece by piece during the next days/weeks.
|
||||||
|
So I am already sorry in advance if one of your desired components you wanted to install or use temporarily cannot be installed or used right now.
|
||||||
|
|
||||||
|
The following functions are currently unavailable:
|
||||||
|
- Installation of: KlipperScreen, Obico, Octoprint, MJPG-Streamer, Telegram Bot and PrettyGCode
|
||||||
|
- All backup functions and the Log-Upload
|
||||||
|
|
||||||
|
**So what is working?**\
|
||||||
|
Installation of Klipper, Moonraker, Mainsail and Fluidd. Both, single and multi-instance setups work!\
|
||||||
|
As already said, the rest will follow in the near future. Updating and removal of already installed components should continue to work.
|
||||||
|
|
||||||
|
**What was removed?**\
|
||||||
|
The option to change Klippers configuration directory got removed. From now on it will not be possible anymore to change
|
||||||
|
the configuration directory from within KIAUH and the new filestructure is enforced.
|
||||||
|
|
||||||
|
**What if I don't have an existing Klipper/Moonraker install right now?**\
|
||||||
|
Nothing important to think about, install Klipper and Moonraker. KIAUH will install both of them with the new filestructure.
|
||||||
|
|
||||||
|
**What if I have an existing Klipper/Moonraker install?**\
|
||||||
|
First of all: Backups! Please copy all of your config files and the Moonraker database (it is a hidden folder, usually `~/.moonraker_database`) to a safe location.
|
||||||
|
After that, uninstall Klipper and Moonraker with KIAUH. You can then proceed and re-install both of them with KIAUH again. It is important that you are on KIAUH v5 for that!
|
||||||
|
Once everything is installed again, you need to manually copy your configuration files from the old `~/klipper_config` folder to the new `~/printer_data/config` folder.
|
||||||
|
Previous, by Moonraker created symlinks to folder of the old filestructure will not work anymore, you need to move the files to their new location now!
|
||||||
|
Do the same with the two files inside of `~/.moonraker_database`. Move/copy them into `~/printer_data/database`. If `~/printer_data/database` is already populated with a `data.mdb` and `lock.mdb`
|
||||||
|
delete them or simply overwrite them. Nothing should be lost as those should be empty database files. Anyway, you made backups, right?
|
||||||
|
You can now proceed and restart Moonraker. Either from within Mainsail or Fluidd, or use SSH and execute `sudo systemctl restart moonraker`.
|
||||||
|
If everything went smooth, you should be good to go again. If you see some Moonraker warnings about deprecated options in the `moonraker.conf`, go ahead and resolve them.
|
||||||
|
I will not cover them in detail here. A good source is the Moonraker documentation: https://moonraker.readthedocs.io/en/latest/configuration/
|
||||||
|
|
||||||
|
**What if I have an existing Klipper/Moonraker multi-instance install?**\
|
||||||
|
Pretty much the same steps that are required for single instance installs apply to multi-instance setups. So please go ahead and read the previous paragraph if you didn't already.
|
||||||
|
Make backups of everything first. Then remove and install the desired amount of Klipper and Moonraker instances again.
|
||||||
|
Now you need to move all config and database files to their new locations.\
|
||||||
|
Example with an instance called `printer_1`:\
|
||||||
|
The config files go from `~/klipper_config/printer_1` to `~/printer_1_data/config`.
|
||||||
|
The database files go from `~/.moonraker_database_1` to `~/printer_1_data/database`.
|
||||||
|
Now restart all Moonraker services. You can restart all of them at once if you launch KIAUH, and in the main menu type `restart moonraker` and hit Enter.
|
||||||
|
|
||||||
|
I hope I have covered the most important things. In case you need further support, the official Klipper Discord is a good place to ask for help.
|
||||||
|
|
||||||
|
### 2022-08-15
|
||||||
|
Support for "Obico for Klipper" was added! Huge thanks to [kennethjiang](https://github.com/kennethjiang) for helping me with the implementation!
|
||||||
|
|
||||||
|
### 2022-05-29
|
||||||
|
KIAUH has now reached major version 4 !
|
||||||
|
* feat: Klipper can be installed under Python3 (still considered as experimental)
|
||||||
|
* feat: Klipper can be installed from custom repositories / inofficial forks
|
||||||
|
* feat: Custom instance name for multi instance installations of Klipper
|
||||||
|
* Any other multi instance will share the same name given to the corresponding Klipper instance
|
||||||
|
* E.g. klipper-voron2 -> moonraker-voron2 -> moonraker-telegram-bot-voron2
|
||||||
|
* feat: Option to allow installation of / updating to unstable Mainsail and Fluidd versions
|
||||||
|
* by default only stable versions get installed/updated
|
||||||
|
* feat: Multi-Instance OctoPrint installations now each have their own virtual python environment
|
||||||
|
* allows independent installation of plugins for each instance
|
||||||
|
* feat: Implementing the use of shellcheck during development
|
||||||
|
* feat: Implementing a simple logging mechanic
|
||||||
|
* feat: Log-upload function now also allows uploading other logfiles (kiauh.log, webcamd.log etc.)
|
||||||
|
* feat: added several new help dialogs which try to explain various functions
|
||||||
|
* fix: During Klipper installation, checks for group membership of `tty` and `dialout` are made
|
||||||
|
* refactor: rework of the settings menu for better control the new KIAUH features
|
||||||
|
* refactor: Support for DWC and DWC-for-Klipper has been removed
|
||||||
|
* refactor: The backup before update settings were moved to the KIAUH settings menu
|
||||||
|
* refactor: Switch branch function has been removed (was replaced by the custom Klipper repo feature)
|
||||||
|
* refactor: The update manager sections for Mainsail, Fluidd and KlipperScreen were removed from the moonraker.conf template
|
||||||
|
* They will now be individually added during installation of the corresponding interface
|
||||||
|
* refactor: The rollback function was reworked and now also allows rollbacks of Moonraker
|
||||||
|
* It now takes numerical inputs and reverts the corresponding repository by the given amount instead
|
||||||
|
* KIAUH does not save previous states to its config anymore like it did with the previous approach
|
||||||
|
|
||||||
|
|
||||||
|
### 2022-01-29
|
||||||
|
* Starting from the 28th of January, Moonraker can make use of PackageKit and PolicyKit.\
|
||||||
|
More details on that can be found [here](
|
||||||
|
https://github.com/Arksine/moonraker/issues/349) and [here](https://github.com/Arksine/moonraker/pull/346)
|
||||||
|
* KIAUH will install Moonrakers PolicyKit rules by default when __installing__ Moonraker
|
||||||
|
* KIAUH will also install Moonrakers PolicyKit rules when __updating__ Moonraker __via KIAUH__ as of now
|
||||||
|
|
||||||
|
### 2021-12-30
|
||||||
|
* Updated the doc for the usage of the [G-Code Shell Command Extension](docs/gcode_shell_command.md)
|
||||||
|
* It became apparent, that some user groups are missing on some systems. A missing video group \
|
||||||
|
membership for example caused issues when installing mjpg-streamer while not using the default pi user. \
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 2021-09-28
|
||||||
|
* New Feature! Added an installer for the Telegram Bot for Moonraker by [nlef](https://github.com/nlef).
|
||||||
|
Checkout his project! Remember to report all issues and/or bugs regarding that project in its corresponding repo and not here 😛.\
|
||||||
|
You can find it here: https://github.com/nlef/moonraker-telegram-bot
|
||||||
|
|
||||||
|
### 2021-09-24
|
||||||
|
* The flashing function got adjusted a bit. It is now possible to also flash controllers which are connected over UART and thus accessible via `/dev/ttyAMA0`. You now have to select a connection methop prior flashing which is either USB or UART.
|
||||||
|
* Due to several requests over time I have now created a Ko-fi account for those who want to support this project and my work with a small donation. Many thanks in advance to all future donors. You can support me on Ko-fi with this link: https://ko-fi.com/th33xitus
|
||||||
|
* As usual, if you find any bugs or issues please report them. I tested the little rework i did with the hardware i have available and haven't encountered any malfunctions of flashing them yet.
|
||||||
|
|
||||||
|
### 2021-08-10
|
||||||
|
* KIAUH now supports the installation of the "PrettyGCode for Klipper" GCode-Viewer created by [Kragrathea](https://github.com/Kragrathea)! Installation, updating and removal are possible with KIAUH. For more details to this cool piece of software, please have a look here: https://github.com/Kragrathea/pgcode
|
||||||
|
|
||||||
|
### 2021-07-10
|
||||||
|
* The NGINX configuration files got updated to be in sync with MainsailOS and FluiddPi. Issues with the NGINX service not starting up due to wrong configuration should be resolved now. To get the updated configuration files, please remove Moonraker and Mainsail / Fluidd with KIAUH first and then re-install it. An automated file check for those configuration files might follow in the future which then automates updating those files if there were important changes.
|
||||||
|
|
||||||
|
* The default `moonraker.conf` was updated to reflect the recent changes to the update manager section. The update channel is set to `dev`.
|
||||||
|
|
||||||
|
### 2021-06-29
|
||||||
|
* KIAUH will now patch the new `log_path` to existing moonraker.conf files when updating Moonraker and the entry is missing. Before that, it was necessary that the user provided that path manually to make Fluidd display the logfiles in its interface. This issue should be resolved now.
|
||||||
|
|
||||||
|
### 2021-06-15
|
||||||
|
|
||||||
|
* Moonraker introduced an optional `log_path` which clients can make use of to show log files located in that folder to their users. More info here: https://github.com/Arksine/moonraker/commit/829b3a4ee80579af35dd64a37ccc092a1f67682a \
|
||||||
|
Client developers agreed upon using `~/klipper_logs` as the new default log path.\
|
||||||
|
That means, from now on, Klipper and Moonraker services installed with KIAUH will place their logfiles in that mentioned folder.
|
||||||
|
* Additionally, KIAUH will now detect Klipper and Moonraker systemd services that still use the old default location of `/tmp/<service>.log` and will update them next time the user updates Klipper and/or Moonraker with the KIAUH update function.
|
||||||
|
* Additional symlinks for the following logfiles will get created along those update procedures to make them accessible through the webinterface once its supported:
|
||||||
|
- webcamd.log
|
||||||
|
- mainsail-access.log
|
||||||
|
- mainsail-error.log
|
||||||
|
- fluidd-access.log
|
||||||
|
- fluidd-error.log
|
||||||
|
* For MainsailOS and FluiddPi users:\
|
||||||
|
MainsailOS and FluiddPi will switch the shipped Klipper service from SysVinit to systemd probably with their next release. KIAUH can already help migrate older MainsailOS (0.4.0 and below) and FluiddPi (v1.13.0) releases to match their new service-, file- and folder-structure so you don't have to re-flash the SD-Card of your Raspberry Pi.\
|
||||||
|
In detail here is what is going to happen when you use the new "CustomPiOS Migration Helper" from the Advanced Menu\
|
||||||
|
`(Main Menu -> 4 -> Enter -> 10 -> Enter)` in a short summary:
|
||||||
|
* The Klipper SysVinit service will get replaced by a Klipper systemd service
|
||||||
|
* Klipper and Moonraker will use the new log-directory `~/klipper_logs`
|
||||||
|
* The webcamd service gets updated
|
||||||
|
* The webcamd script gets updated and moved from `/root/bin/webcamd` to `/usr/local/bin/webcamd`
|
||||||
|
* The NGINX `upstreams.conf` gets updated to be able to configure up to 4 webcams
|
||||||
|
* The `mainsail.txt` / `fluiddpi.txt` gets moved from `/boot` to `~/klipper_config` and renamed to `webcam.txt`
|
||||||
|
* Symlinks for the webcamd.log and various NGINX logs get created in `~/klipper_config`
|
||||||
|
* Configuration files for Klipper, Moonraker and webcamd get added to `/etc/logrotate.d`
|
||||||
|
* If they still exist, two lines will be removed from the mainsail.cfg or client_macros.cfg macro configurations:\
|
||||||
|
`SAVE_GCODE_STATE NAME=PAUSE_state` and `RESTORE_GCODE_STATE NAME=PAUSE_state`
|
||||||
|
* **Please note:**\
|
||||||
|
The "CustomPiOS Migration Helper" is intended to only work on "vanilla" MainsailOS and FluiddPi systems. Do not try to migrate a modified MainsailOS or FluiddPi system (for example if you already used KIAUH to re-install services or to set up a multi-instance installation for Klipper / Moonraker). This won't work.
|
||||||
|
|
||||||
### 2021-01-31
|
### 2021-01-31
|
||||||
|
|
||||||
* **This is a big one... KIAUH v3.0 is out.**\
|
* **This is a big one... KIAUH v3.0 is out.**\
|
||||||
@@ -46,9 +213,9 @@ Each service gets its corresponding instance added to the service filename.
|
|||||||
--> moonraker-2.service
|
--> moonraker-2.service
|
||||||
--> moonraker-n.service
|
--> moonraker-n.service
|
||||||
```
|
```
|
||||||
* The same service file rules from above apply to DWC and OctoPrint even though only Klipper and Moonraker are shown in this example.
|
* The same service file rules from above apply to OctoPrint even though only Klipper and Moonraker are shown in this example.
|
||||||
|
|
||||||
* You can start, stop and restart all Klipper, Moonraker, DWC and OctoPrint instances from the KIAUH main menu. For doing this, just type "stop klipper", "start moonraker", "restart octoprint" and so on.
|
* You can start, stop and restart all Klipper, Moonraker and OctoPrint instances from the KIAUH main menu. For doing this, just type "stop klipper", "start moonraker", "restart octoprint" and so on.
|
||||||
|
|
||||||
* KIAUH v3.0 relocated its ini-file. It is now a hidden file in the users home-directory calles `.kiauh.ini`. This has the benefit of keeping all values in that file between possible re-installations of KIAUH. Otherwise that file would be lost.
|
* KIAUH v3.0 relocated its ini-file. It is now a hidden file in the users home-directory calles `.kiauh.ini`. This has the benefit of keeping all values in that file between possible re-installations of KIAUH. Otherwise that file would be lost.
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# Feature List:
|
|
||||||
|
|
||||||
- **Automatic dependency check:**\
|
|
||||||
If packages are missing but needed for the asked task, the script will automatically install them
|
|
||||||
- **Switch between different Klipper Forks:**\
|
|
||||||
[origin/master](https://github.com/KevinOConnor/klipper/tree/master) or [scurve-shaping](https://github.com/dmbutyugin/klipper/tree/scurve-shaping) or [scurve-smoothing](https://github.com/dmbutyugin/klipper/tree/scurve-smoothing)\
|
|
||||||
The update function of the script will always update the currently selected/active fork!
|
|
||||||
- **Toggle auto-create backups before updating:**\
|
|
||||||
When enabled, a backup of the installation you want to update is made prior updating
|
|
||||||
- **Rollback:**\
|
|
||||||
When updating Klipper, KIAUH saves the current commit hash to a local ini-file. In case of an unsuccesfull update you can use this function to quickly revert back to the commit with the hash you updated from.
|
|
||||||
- **Preconfigure OctoPrint:**\
|
|
||||||
When installing OctoPrint, a config is created which preconfigures your installation to be used with Klipper.\
|
|
||||||
That means:
|
|
||||||
- adding the restart/shutdown commands for OctoPrint
|
|
||||||
- adding the serial port `/tmp/printer`
|
|
||||||
- set the behavior to "Cancel any ongoing prints but stay connected to the printer"
|
|
||||||
- **Enable/Disable OctoPrint Service:**\
|
|
||||||
Usefull when using DWC2/Mainsail/Fluidd and OctoPrint at the same time to prevent them interfering with each other
|
|
||||||
|
|
||||||
- **Installing a G-Code Shell Command extension:**\
|
|
||||||
For further information about that extension please see the [G-Code Shell Command Extension Doc](gcode_shell_command.md)
|
|
||||||
|
|
||||||
- **Uploading logfiles:**\
|
|
||||||
You can directly upload logfiles like klippy.log, moonraker.log and dwc2.log from the KIAUH main menu for providing them for troubleshooting purposes.
|
|
||||||
|
|
||||||
|
|
||||||
to be continued...
|
|
||||||
@@ -40,6 +40,34 @@ verbose: True
|
|||||||
Execute with:
|
Execute with:
|
||||||
`RUN_SHELL_COMMAND CMD=hello_world`
|
`RUN_SHELL_COMMAND CMD=hello_world`
|
||||||
|
|
||||||
|
### Passing parameters:
|
||||||
|
As of commit [f231fa9](https://github.com/dw-0/kiauh/commit/f231fa9c69191f23277b4e3319f6b675bfa0ee42) it is also possible to pass optional parameters to a `gcode_shell_command`.
|
||||||
|
The following short example shows storing the extruder temperature into a variable, passing that value with a parameter to a `gcode_shell_command`, which then,
|
||||||
|
once the gcode_macro runs and the gcode_shell_command gets called, executes the `script.sh`. The script then echoes a message to the console (if `verbose: True`)
|
||||||
|
and writes the value of the parameter into a textfile called `test.txt` located in the home directory.
|
||||||
|
|
||||||
|
Content of the `gcode_shell_command` and the `gcode_macro`:
|
||||||
|
```
|
||||||
|
[gcode_shell_command print_to_file]
|
||||||
|
command: sh /home/pi/klipper_config/script.sh
|
||||||
|
timeout: 30.
|
||||||
|
verbose: True
|
||||||
|
|
||||||
|
[gcode_macro GET_TEMP]
|
||||||
|
gcode:
|
||||||
|
{% set temp = printer.extruder.temperature %}
|
||||||
|
{ action_respond_info("%s" % (temp)) }
|
||||||
|
RUN_SHELL_COMMAND CMD=print_to_file PARAMS={temp}
|
||||||
|
```
|
||||||
|
|
||||||
|
Content of `script.sh`:
|
||||||
|
```shell
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "temp is: $1"
|
||||||
|
echo "$1" >> "${HOME}/test.txt"
|
||||||
|
```
|
||||||
|
|
||||||
## Warning
|
## Warning
|
||||||
|
|
||||||
This extension may have a high potential for abuse if not used carefully! Also, depending on the command you execute, high system loads may occur and can cause system instabilities.
|
This extension may have a high potential for abuse if not used carefully! Also, depending on the command you execute, high system loads may occur and can cause system instabilities.
|
||||||
|
|||||||
15
kiauh.py
Normal file
15
kiauh.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# ======================================================================= #
|
||||||
|
# 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 kiauh.main import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
195
kiauh.sh
195
kiauh.sh
@@ -1,101 +1,108 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
clear
|
|
||||||
|
#=======================================================================#
|
||||||
|
# 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 #
|
||||||
|
#=======================================================================#
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
clear
|
||||||
|
|
||||||
### set color variables
|
function main() {
|
||||||
green=$(echo -en "\e[92m")
|
local python_command
|
||||||
yellow=$(echo -en "\e[93m")
|
local entrypoint
|
||||||
red=$(echo -en "\e[91m")
|
|
||||||
cyan=$(echo -en "\e[96m")
|
|
||||||
default=$(echo -en "\e[39m")
|
|
||||||
|
|
||||||
### sourcing all additional scripts
|
if command -v python3 &>/dev/null; then
|
||||||
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
python_command="python3"
|
||||||
for script in "${SRCDIR}/kiauh/scripts/"*.sh; do . $script; done
|
elif command -v python &>/dev/null; then
|
||||||
for script in "${SRCDIR}/kiauh/scripts/ui/"*.sh; do . $script; done
|
python_command="python"
|
||||||
|
else
|
||||||
### set important directories
|
echo "Python is not installed. Please install Python and try again."
|
||||||
#klipper
|
exit 1
|
||||||
KLIPPER_DIR=${HOME}/klipper
|
|
||||||
KLIPPY_ENV=${HOME}/klippy-env
|
|
||||||
#nginx
|
|
||||||
NGINX_SA=/etc/nginx/sites-available
|
|
||||||
NGINX_SE=/etc/nginx/sites-enabled
|
|
||||||
NGINX_CONFD=/etc/nginx/conf.d
|
|
||||||
#moonraker
|
|
||||||
MOONRAKER_DIR=${HOME}/moonraker
|
|
||||||
MOONRAKER_ENV=${HOME}/moonraker-env
|
|
||||||
#mainsail
|
|
||||||
MAINSAIL_DIR=${HOME}/mainsail
|
|
||||||
#fluidd
|
|
||||||
FLUIDD_DIR=${HOME}/fluidd
|
|
||||||
#dwc2
|
|
||||||
DWC2FK_DIR=${HOME}/dwc2-for-klipper-socket
|
|
||||||
DWC_ENV_DIR=${HOME}/dwc-env
|
|
||||||
DWC2_DIR=${HOME}/duetwebcontrol
|
|
||||||
#octoprint
|
|
||||||
OCTOPRINT_DIR=${HOME}/OctoPrint
|
|
||||||
#KlipperScreen
|
|
||||||
KLIPPERSCREEN_DIR=${HOME}/KlipperScreen
|
|
||||||
KLIPPERSCREEN_ENV_DIR=${HOME}/.KlipperScreen-env
|
|
||||||
#misc
|
|
||||||
INI_FILE=${HOME}/.kiauh.ini
|
|
||||||
BACKUP_DIR=${HOME}/kiauh-backups
|
|
||||||
|
|
||||||
### set github repos
|
|
||||||
KLIPPER_REPO=https://github.com/KevinOConnor/klipper.git
|
|
||||||
ARKSINE_REPO=https://github.com/Arksine/klipper.git
|
|
||||||
DMBUTYUGIN_REPO=https://github.com/dmbutyugin/klipper.git
|
|
||||||
DWC2FK_REPO=https://github.com/Stephan3/dwc2-for-klipper-socket.git
|
|
||||||
MOONRAKER_REPO=https://github.com/Arksine/moonraker.git
|
|
||||||
KLIPPERSCREEN_REPO=https://github.com/jordanruthe/KlipperScreen.git
|
|
||||||
#branches
|
|
||||||
BRANCH_SCURVE_SMOOTHING=dmbutyugin/scurve-smoothing
|
|
||||||
BRANCH_SCURVE_SHAPING=dmbutyugin/scurve-shaping
|
|
||||||
|
|
||||||
### set some messages
|
|
||||||
warn_msg(){
|
|
||||||
echo -e "${red}<!!!!> $1${default}"
|
|
||||||
}
|
|
||||||
status_msg(){
|
|
||||||
echo; echo -e "${yellow}###### $1${default}"
|
|
||||||
}
|
|
||||||
ok_msg(){
|
|
||||||
echo -e "${green}>>>>>> $1${default}"
|
|
||||||
}
|
|
||||||
title_msg(){
|
|
||||||
echo -e "${cyan}$1${default}"
|
|
||||||
}
|
|
||||||
get_date(){
|
|
||||||
current_date=$(date +"%y%m%d-%H%M")
|
|
||||||
}
|
|
||||||
print_unkown_cmd(){
|
|
||||||
ERROR_MSG="Invalid command!"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_msg(){
|
|
||||||
if [[ "$ERROR_MSG" != "" ]]; then
|
|
||||||
echo -e "${red}"
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e " $ERROR_MSG "
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e "${default}"
|
|
||||||
fi
|
|
||||||
if [ "$CONFIRM_MSG" != "" ]; then
|
|
||||||
echo -e "${green}"
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e " $CONFIRM_MSG "
|
|
||||||
echo -e "#########################################################"
|
|
||||||
echo -e "${default}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
entrypoint=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||||
|
|
||||||
|
${python_command} "${entrypoint}/kiauh.py"
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_msg(){
|
main
|
||||||
unset CONFIRM_MSG
|
|
||||||
unset ERROR_MSG
|
|
||||||
}
|
|
||||||
|
|
||||||
check_euid
|
#### sourcing all additional scripts
|
||||||
init_ini
|
#KIAUH_SRCDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||||
kiauh_status
|
#for script in "${KIAUH_SRCDIR}/scripts/"*.sh; do . "${script}"; done
|
||||||
main_menu
|
#for script in "${KIAUH_SRCDIR}/scripts/ui/"*.sh; do . "${script}"; done
|
||||||
|
#
|
||||||
|
##===================================================#
|
||||||
|
##=================== UPDATE KIAUH ==================#
|
||||||
|
##===================================================#
|
||||||
|
#
|
||||||
|
#function update_kiauh() {
|
||||||
|
# status_msg "Updating KIAUH ..."
|
||||||
|
#
|
||||||
|
# cd "${KIAUH_SRCDIR}"
|
||||||
|
# git reset --hard && git pull
|
||||||
|
#
|
||||||
|
# ok_msg "Update complete! Please restart KIAUH."
|
||||||
|
# exit 0
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
##===================================================#
|
||||||
|
##=================== KIAUH STATUS ==================#
|
||||||
|
##===================================================#
|
||||||
|
#
|
||||||
|
#function kiauh_update_avail() {
|
||||||
|
# [[ ! -d "${KIAUH_SRCDIR}/.git" ]] && return
|
||||||
|
# local origin head
|
||||||
|
#
|
||||||
|
# cd "${KIAUH_SRCDIR}"
|
||||||
|
#
|
||||||
|
# ### abort if not on master branch
|
||||||
|
# ! git branch -a | grep -q "\* master" && return
|
||||||
|
#
|
||||||
|
# ### compare commit hash
|
||||||
|
# git fetch -q
|
||||||
|
# origin=$(git rev-parse --short=8 origin/master)
|
||||||
|
# head=$(git rev-parse --short=8 HEAD)
|
||||||
|
#
|
||||||
|
# if [[ ${origin} != "${head}" ]]; then
|
||||||
|
# echo "true"
|
||||||
|
# fi
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#function kiauh_update_dialog() {
|
||||||
|
# [[ ! $(kiauh_update_avail) == "true" ]] && return
|
||||||
|
# top_border
|
||||||
|
# echo -e "|${green} New KIAUH update available! ${white}|"
|
||||||
|
# hr
|
||||||
|
# echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|"
|
||||||
|
# blank_line
|
||||||
|
# echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|"
|
||||||
|
# echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|"
|
||||||
|
# echo -e "|${yellow} features. Please consider updating! ${white}|"
|
||||||
|
# bottom_border
|
||||||
|
#
|
||||||
|
# local yn
|
||||||
|
# read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn
|
||||||
|
# while true; do
|
||||||
|
# case "${yn}" in
|
||||||
|
# Y|y|Yes|yes|"")
|
||||||
|
# do_action "update_kiauh"
|
||||||
|
# break;;
|
||||||
|
# N|n|No|no)
|
||||||
|
# break;;
|
||||||
|
# *)
|
||||||
|
# deny_action "kiauh_update_dialog";;
|
||||||
|
# esac
|
||||||
|
# done
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#check_euid
|
||||||
|
#init_logfile
|
||||||
|
#set_globals
|
||||||
|
#kiauh_update_dialog
|
||||||
|
#main_menu
|
||||||
|
|||||||
15
kiauh/__init__.py
Normal file
15
kiauh/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
APPLICATION_ROOT = Path(__file__).resolve().parent
|
||||||
|
sys.path.append(str(APPLICATION_ROOT))
|
||||||
0
kiauh/components/__init__.py
Normal file
0
kiauh/components/__init__.py
Normal file
16
kiauh/components/crowsnest/__init__.py
Normal file
16
kiauh/components/crowsnest/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
CROWSNEST_DIR = Path.home().joinpath("crowsnest")
|
||||||
|
CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git"
|
||||||
|
CROWSNEST_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("crowsnest-backups")
|
||||||
173
kiauh/components/crowsnest/crowsnest.py
Normal file
173
kiauh/components/crowsnest/crowsnest.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.crowsnest import CROWSNEST_BACKUP_DIR, CROWSNEST_DIR, CROWSNEST_REPO
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.common import (
|
||||||
|
check_install_dependencies,
|
||||||
|
get_install_status,
|
||||||
|
)
|
||||||
|
from utils.constants import CURRENT_USER
|
||||||
|
from utils.git_utils import (
|
||||||
|
git_clone_wrapper,
|
||||||
|
git_pull_wrapper,
|
||||||
|
)
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import DialogType, Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
cmd_sysctl_service,
|
||||||
|
parse_packages_from_file,
|
||||||
|
)
|
||||||
|
from utils.types import ComponentStatus
|
||||||
|
|
||||||
|
|
||||||
|
def install_crowsnest() -> None:
|
||||||
|
# Step 1: Clone crowsnest repo
|
||||||
|
git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master")
|
||||||
|
|
||||||
|
# Step 2: Install dependencies
|
||||||
|
check_install_dependencies(["make"])
|
||||||
|
|
||||||
|
# Step 3: Check for Multi Instance
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
instances: List[Klipper] = im.instances
|
||||||
|
|
||||||
|
if len(instances) > 1:
|
||||||
|
print_multi_instance_warning(instances)
|
||||||
|
|
||||||
|
if not get_confirm("Do you want to continue with the installation?"):
|
||||||
|
Logger.print_info("Crowsnest installation aborted!")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Launching crowsnest's install configurator ...")
|
||||||
|
time.sleep(3)
|
||||||
|
configure_multi_instance()
|
||||||
|
|
||||||
|
# Step 4: Launch crowsnest installer
|
||||||
|
Logger.print_status("Launching crowsnest installer ...")
|
||||||
|
Logger.print_info("Installer will prompt you for sudo password!")
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
f"sudo make install BASE_USER={CURRENT_USER}",
|
||||||
|
cwd=CROWSNEST_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def print_multi_instance_warning(instances: List[Klipper]) -> None:
|
||||||
|
_instances = [f"● {instance.data_dir_name}" for instance in instances]
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.WARNING,
|
||||||
|
[
|
||||||
|
"Multi instance install detected!",
|
||||||
|
"\n\n",
|
||||||
|
"Crowsnest is NOT designed to support multi instances. A workaround "
|
||||||
|
"for this is to choose the most used instance as a 'master' and use "
|
||||||
|
"this instance to set up your 'crowsnest.conf' and steering it's service.",
|
||||||
|
"\n\n",
|
||||||
|
"The following instances were found:",
|
||||||
|
*_instances,
|
||||||
|
],
|
||||||
|
end="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_multi_instance() -> None:
|
||||||
|
config = Path(CROWSNEST_DIR).joinpath("tools/.config")
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make config",
|
||||||
|
cwd=CROWSNEST_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
|
||||||
|
if config.exists():
|
||||||
|
Path.unlink(config)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not config.exists():
|
||||||
|
Logger.print_error("Generating .config failed, installation aborted")
|
||||||
|
|
||||||
|
|
||||||
|
def update_crowsnest() -> None:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service("crowsnest", "stop")
|
||||||
|
|
||||||
|
if not CROWSNEST_DIR.exists():
|
||||||
|
git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master")
|
||||||
|
else:
|
||||||
|
Logger.print_status("Updating Crowsnest ...")
|
||||||
|
|
||||||
|
settings = KiauhSettings()
|
||||||
|
if settings.get("kiauh", "backup_before_update"):
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(
|
||||||
|
"crowsnest",
|
||||||
|
source=CROWSNEST_DIR,
|
||||||
|
target=CROWSNEST_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
git_pull_wrapper(CROWSNEST_REPO, CROWSNEST_DIR)
|
||||||
|
|
||||||
|
script = CROWSNEST_DIR.joinpath("tools/install.sh")
|
||||||
|
deps = parse_packages_from_file(script)
|
||||||
|
check_install_dependencies(deps)
|
||||||
|
|
||||||
|
cmd_sysctl_service("crowsnest", "restart")
|
||||||
|
|
||||||
|
Logger.print_ok("Crowsnest updated successfully.", end="\n\n")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def get_crowsnest_status() -> ComponentStatus:
|
||||||
|
files = [
|
||||||
|
Path("/usr/local/bin/crowsnest"),
|
||||||
|
Path("/etc/logrotate.d/crowsnest"),
|
||||||
|
Path("/etc/systemd/system/crowsnest.service"),
|
||||||
|
]
|
||||||
|
return get_install_status(CROWSNEST_DIR, files=files)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_crowsnest() -> None:
|
||||||
|
if not CROWSNEST_DIR.exists():
|
||||||
|
Logger.print_info("Crowsnest does not seem to be installed! Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make uninstall",
|
||||||
|
cwd=CROWSNEST_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Removing crowsnest directory ...")
|
||||||
|
shutil.rmtree(CROWSNEST_DIR)
|
||||||
|
Logger.print_ok("Directory removed!")
|
||||||
21
kiauh/components/klipper/__init__.py
Normal file
21
kiauh/components/klipper/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
KLIPPER_DIR = Path.home().joinpath("klipper")
|
||||||
|
KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env")
|
||||||
|
KLIPPER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipper-backups")
|
||||||
|
KLIPPER_REQUIREMENTS_TXT = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt")
|
||||||
|
|
||||||
|
EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..."
|
||||||
1
kiauh/components/klipper/assets/klipper.env
Normal file
1
kiauh/components/klipper/assets/klipper.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %SERIAL% -l %LOG% -a %UDS%"
|
||||||
18
kiauh/components/klipper/assets/klipper.service
Normal file
18
kiauh/components/klipper/assets/klipper.service
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Klipper 3D Printer Firmware SV1
|
||||||
|
Documentation=https://www.klipper3d.org/
|
||||||
|
After=network-online.target
|
||||||
|
Wants=udev.target
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=%USER%
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=%KLIPPER_DIR%
|
||||||
|
EnvironmentFile=%ENV_FILE%
|
||||||
|
ExecStart=%ENV%/bin/python $KLIPPER_ARGS
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
11
kiauh/components/klipper/assets/printer.cfg
Normal file
11
kiauh/components/klipper/assets/printer.cfg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[mcu]
|
||||||
|
serial: /dev/serial/by-id/<your-mcu-id>
|
||||||
|
|
||||||
|
[virtual_sdcard]
|
||||||
|
path: %GCODES_DIR%
|
||||||
|
on_error_gcode: CANCEL_PRINT
|
||||||
|
|
||||||
|
[printer]
|
||||||
|
kinematics: none
|
||||||
|
max_velocity: 1000
|
||||||
|
max_accel: 1000
|
||||||
152
kiauh/components/klipper/klipper.py
Normal file
152
kiauh/components/klipper/klipper.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR, MODULE_PATH
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class Klipper(BaseInstance):
|
||||||
|
@classmethod
|
||||||
|
def blacklist(cls) -> List[str]:
|
||||||
|
return ["None", "mcu"]
|
||||||
|
|
||||||
|
def __init__(self, suffix: str = ""):
|
||||||
|
super().__init__(instance_type=self, suffix=suffix)
|
||||||
|
self.klipper_dir: Path = KLIPPER_DIR
|
||||||
|
self.env_dir: Path = KLIPPER_ENV_DIR
|
||||||
|
self._cfg_file = self.cfg_dir.joinpath("printer.cfg")
|
||||||
|
self._log = self.log_dir.joinpath("klippy.log")
|
||||||
|
self._serial = self.comms_dir.joinpath("klippy.serial")
|
||||||
|
self._uds = self.comms_dir.joinpath("klippy.sock")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cfg_file(self) -> Path:
|
||||||
|
return self._cfg_file
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log(self) -> Path:
|
||||||
|
return self._log
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial(self) -> Path:
|
||||||
|
return self._serial
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uds(self) -> Path:
|
||||||
|
return self._uds
|
||||||
|
|
||||||
|
def create(self) -> None:
|
||||||
|
Logger.print_status("Creating new Klipper Instance ...")
|
||||||
|
service_template_path = MODULE_PATH.joinpath("assets/klipper.service")
|
||||||
|
service_file_name = self.get_service_file_name(extension=True)
|
||||||
|
service_file_target = SYSTEMD.joinpath(service_file_name)
|
||||||
|
env_template_file_path = MODULE_PATH.joinpath("assets/klipper.env")
|
||||||
|
env_file_target = self.sysd_dir.joinpath("klipper.env")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.create_folders()
|
||||||
|
self.write_service_file(
|
||||||
|
service_template_path, service_file_target, env_file_target
|
||||||
|
)
|
||||||
|
self.write_env_file(env_template_file_path, env_file_target)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Error creating service file {service_file_target}: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Error creating env file {env_file_target}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
service_file = self.get_service_file_name(extension=True)
|
||||||
|
service_file_path = self.get_service_file_path()
|
||||||
|
|
||||||
|
Logger.print_status(f"Deleting Klipper Instance: {service_file}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = ["sudo", "rm", "-f", service_file_path]
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
Logger.print_ok(f"Service file deleted: {service_file_path}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error deleting service file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def write_service_file(
|
||||||
|
self,
|
||||||
|
service_template_path: Path,
|
||||||
|
service_file_target: Path,
|
||||||
|
env_file_target: Path,
|
||||||
|
) -> None:
|
||||||
|
service_content = self._prep_service_file(
|
||||||
|
service_template_path, env_file_target
|
||||||
|
)
|
||||||
|
command = ["sudo", "tee", service_file_target]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
input=service_content.encode(),
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
Logger.print_ok(f"Service file created: {service_file_target}")
|
||||||
|
|
||||||
|
def write_env_file(
|
||||||
|
self, env_template_file_path: Path, env_file_target: Path
|
||||||
|
) -> None:
|
||||||
|
env_file_content = self._prep_env_file(env_template_file_path)
|
||||||
|
with open(env_file_target, "w") as env_file:
|
||||||
|
env_file.write(env_file_content)
|
||||||
|
Logger.print_ok(f"Env file created: {env_file_target}")
|
||||||
|
|
||||||
|
def _prep_service_file(
|
||||||
|
self, service_template_path: Path, env_file_path: Path
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
with open(service_template_path, "r") as template_file:
|
||||||
|
template_content = template_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {service_template_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
service_content = template_content.replace("%USER%", self.user)
|
||||||
|
service_content = service_content.replace(
|
||||||
|
"%KLIPPER_DIR%", str(self.klipper_dir)
|
||||||
|
)
|
||||||
|
service_content = service_content.replace("%ENV%", str(self.env_dir))
|
||||||
|
service_content = service_content.replace("%ENV_FILE%", str(env_file_path))
|
||||||
|
return service_content
|
||||||
|
|
||||||
|
def _prep_env_file(self, env_template_file_path: Path) -> str:
|
||||||
|
try:
|
||||||
|
with open(env_template_file_path, "r") as env_file:
|
||||||
|
env_template_file_content = env_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {env_template_file_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
env_file_content = env_template_file_content.replace(
|
||||||
|
"%KLIPPER_DIR%", str(self.klipper_dir)
|
||||||
|
)
|
||||||
|
env_file_content = env_file_content.replace(
|
||||||
|
"%CFG%", f"{self.cfg_dir}/printer.cfg"
|
||||||
|
)
|
||||||
|
env_file_content = env_file_content.replace("%SERIAL%", str(self.serial))
|
||||||
|
env_file_content = env_file_content.replace("%LOG%", str(self.log))
|
||||||
|
env_file_content = env_file_content.replace("%UDS%", str(self.uds))
|
||||||
|
return env_file_content
|
||||||
151
kiauh/components/klipper/klipper_dialogs.py
Normal file
151
kiauh/components/klipper/klipper_dialogs.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from enum import Enum, unique
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.menus.base_menu import print_back_footer
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class DisplayType(Enum):
|
||||||
|
SERVICE_NAME = "SERVICE_NAME"
|
||||||
|
PRINTER_NAME = "PRINTER_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
def print_instance_overview(
|
||||||
|
instances: List[BaseInstance],
|
||||||
|
display_type: DisplayType = DisplayType.SERVICE_NAME,
|
||||||
|
show_headline=True,
|
||||||
|
show_index=False,
|
||||||
|
show_select_all=False,
|
||||||
|
):
|
||||||
|
dialog = "/=======================================================\\\n"
|
||||||
|
if show_headline:
|
||||||
|
d_type = (
|
||||||
|
"Klipper instances"
|
||||||
|
if display_type is DisplayType.SERVICE_NAME
|
||||||
|
else "printer directories"
|
||||||
|
)
|
||||||
|
headline = f"{COLOR_GREEN}The following {d_type} were found:{RESET_FORMAT}"
|
||||||
|
dialog += f"|{headline:^64}|\n"
|
||||||
|
dialog += "|-------------------------------------------------------|\n"
|
||||||
|
|
||||||
|
if show_select_all:
|
||||||
|
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
|
||||||
|
dialog += f"| {select_all:<63}|\n"
|
||||||
|
dialog += "| |\n"
|
||||||
|
|
||||||
|
for i, s in enumerate(instances):
|
||||||
|
if display_type is DisplayType.SERVICE_NAME:
|
||||||
|
name = s.get_service_file_name()
|
||||||
|
else:
|
||||||
|
name = s.data_dir
|
||||||
|
line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {name}{RESET_FORMAT}"
|
||||||
|
dialog += f"| {line:<63}|\n"
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_select_instance_count_dialog():
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}Setting up too many instances may crash your system.{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| Please select the number of Klipper instances to set |
|
||||||
|
| up. The number of Klipper instances will determine |
|
||||||
|
| the amount of printers you can run from this host. |
|
||||||
|
| |
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_select_custom_name_dialog():
|
||||||
|
line1 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}Only alphanumeric characters are allowed!{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| You can now assign a custom name to each instance. |
|
||||||
|
| If skipped, each instance will get an index assigned |
|
||||||
|
| in ascending order, starting at index '1'. |
|
||||||
|
| |
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_missing_usergroup_dialog(missing_groups) -> None:
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING: Your current user is not in group:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_CYAN}● tty{RESET_FORMAT}"
|
||||||
|
line3 = f"{COLOR_CYAN}● dialout{RESET_FORMAT}"
|
||||||
|
line4 = f"{COLOR_YELLOW}INFO:{RESET_FORMAT}"
|
||||||
|
line5 = f"{COLOR_YELLOW}Relog required for group assignments to take effect!{RESET_FORMAT}"
|
||||||
|
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if "tty" in missing_groups:
|
||||||
|
dialog += f"| {line2:<63}|\n"
|
||||||
|
if "dialout" in missing_groups:
|
||||||
|
dialog += f"| {line3:<63}|\n"
|
||||||
|
|
||||||
|
dialog += textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
| |
|
||||||
|
| It is possible that you won't be able to successfully |
|
||||||
|
| connect and/or flash the controller board without |
|
||||||
|
| your user being a member of that group. |
|
||||||
|
| If you want to add the current user to the group(s) |
|
||||||
|
| listed above, answer with 'Y'. Else skip with 'n'. |
|
||||||
|
| |
|
||||||
|
| {line4:<63}|
|
||||||
|
| {line5:<63}|
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_update_warn_dialog() -> None:
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}Do NOT continue if there are ongoing prints running!{RESET_FORMAT}"
|
||||||
|
line3 = f"{COLOR_YELLOW}All Klipper instances will be restarted during the {RESET_FORMAT}"
|
||||||
|
line4 = f"{COLOR_YELLOW}update process and ongoing prints WILL FAIL.{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
| {line3:<63}|
|
||||||
|
| {line4:<63}|
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
131
kiauh/components/klipper/klipper_remove.py
Normal file
131
kiauh/components/klipper/klipper_remove.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 #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR, KLIPPER_ENV_DIR
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import print_instance_overview
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.fs_utils import remove_file
|
||||||
|
from utils.input_utils import get_selection_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import cmd_sysctl_manage
|
||||||
|
|
||||||
|
|
||||||
|
def run_klipper_removal(
|
||||||
|
remove_service: bool,
|
||||||
|
remove_dir: bool,
|
||||||
|
remove_env: bool,
|
||||||
|
delete_logs: bool,
|
||||||
|
) -> None:
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
|
||||||
|
if remove_service:
|
||||||
|
Logger.print_status("Removing Klipper instances ...")
|
||||||
|
if im.instances:
|
||||||
|
instances_to_remove = select_instances_to_remove(im.instances)
|
||||||
|
remove_instances(im, instances_to_remove)
|
||||||
|
else:
|
||||||
|
Logger.print_info("No Klipper Services installed! Skipped ...")
|
||||||
|
|
||||||
|
if (remove_dir or remove_env) and im.instances:
|
||||||
|
Logger.print_warn("There are still other Klipper services installed!")
|
||||||
|
Logger.print_warn("Therefor the following parts cannot be removed:")
|
||||||
|
Logger.print_warn(
|
||||||
|
"""
|
||||||
|
● Klipper local repository
|
||||||
|
● Klipper Python environment
|
||||||
|
""",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if remove_dir:
|
||||||
|
Logger.print_status("Removing Klipper local repository ...")
|
||||||
|
remove_klipper_dir()
|
||||||
|
if remove_env:
|
||||||
|
Logger.print_status("Removing Klipper Python environment ...")
|
||||||
|
remove_klipper_env()
|
||||||
|
|
||||||
|
# delete klipper logs of all instances
|
||||||
|
if delete_logs:
|
||||||
|
Logger.print_status("Removing all Klipper logs ...")
|
||||||
|
delete_klipper_logs(im.instances)
|
||||||
|
|
||||||
|
|
||||||
|
def select_instances_to_remove(
|
||||||
|
instances: List[Klipper],
|
||||||
|
) -> Union[List[Klipper], None]:
|
||||||
|
print_instance_overview(instances, show_index=True, show_select_all=True)
|
||||||
|
|
||||||
|
options = [str(i) for i in range(len(instances))]
|
||||||
|
options.extend(["a", "A", "b", "B"])
|
||||||
|
|
||||||
|
selection = get_selection_input("Select Klipper instance to remove", options)
|
||||||
|
|
||||||
|
instances_to_remove = []
|
||||||
|
if selection == "b".lower():
|
||||||
|
return None
|
||||||
|
elif selection == "a".lower():
|
||||||
|
instances_to_remove.extend(instances)
|
||||||
|
else:
|
||||||
|
instance = instances[int(selection)]
|
||||||
|
instances_to_remove.append(instance)
|
||||||
|
|
||||||
|
return instances_to_remove
|
||||||
|
|
||||||
|
|
||||||
|
def remove_instances(
|
||||||
|
instance_manager: InstanceManager,
|
||||||
|
instance_list: List[Klipper],
|
||||||
|
) -> None:
|
||||||
|
for instance in instance_list:
|
||||||
|
Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...")
|
||||||
|
instance_manager.current_instance = instance
|
||||||
|
instance_manager.stop_instance()
|
||||||
|
instance_manager.disable_instance()
|
||||||
|
instance_manager.delete_instance()
|
||||||
|
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_klipper_dir() -> None:
|
||||||
|
if not KLIPPER_DIR.exists():
|
||||||
|
Logger.print_info(f"'{KLIPPER_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(KLIPPER_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{KLIPPER_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_klipper_env() -> None:
|
||||||
|
if not KLIPPER_ENV_DIR.exists():
|
||||||
|
Logger.print_info(f"'{KLIPPER_ENV_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(KLIPPER_ENV_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{KLIPPER_ENV_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_klipper_logs(instances: List[Klipper]) -> None:
|
||||||
|
all_logfiles = []
|
||||||
|
for instance in instances:
|
||||||
|
all_logfiles = list(instance.log_dir.glob("klippy.log*"))
|
||||||
|
if not all_logfiles:
|
||||||
|
Logger.print_info("No Klipper logs found. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
for log in all_logfiles:
|
||||||
|
Logger.print_status(f"Remove '{log}'")
|
||||||
|
remove_file(log)
|
||||||
173
kiauh/components/klipper/klipper_setup.py
Normal file
173
kiauh/components/klipper/klipper_setup.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from components.klipper import (
|
||||||
|
EXIT_KLIPPER_SETUP,
|
||||||
|
KLIPPER_DIR,
|
||||||
|
KLIPPER_ENV_DIR,
|
||||||
|
KLIPPER_REQUIREMENTS_TXT,
|
||||||
|
)
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import print_update_warn_dialog
|
||||||
|
from components.klipper.klipper_utils import (
|
||||||
|
add_to_existing,
|
||||||
|
backup_klipper_dir,
|
||||||
|
check_is_single_to_multi_conversion,
|
||||||
|
check_user_groups,
|
||||||
|
create_example_printer_cfg,
|
||||||
|
get_install_count,
|
||||||
|
handle_disruptive_system_packages,
|
||||||
|
handle_instance_naming,
|
||||||
|
handle_to_multi_instance_conversion,
|
||||||
|
init_name_scheme,
|
||||||
|
update_name_scheme,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
get_existing_clients,
|
||||||
|
)
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.common import check_install_dependencies
|
||||||
|
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
cmd_sysctl_manage,
|
||||||
|
create_python_venv,
|
||||||
|
install_python_requirements,
|
||||||
|
parse_packages_from_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_klipper() -> None:
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
|
||||||
|
# ask to add new instances, if there are existing ones
|
||||||
|
if kl_im.instances and not add_to_existing():
|
||||||
|
Logger.print_status(EXIT_KLIPPER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
install_count = get_install_count()
|
||||||
|
if install_count is None:
|
||||||
|
Logger.print_status(EXIT_KLIPPER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
# create a dict of the size of the existing instances + install count
|
||||||
|
name_dict = {c: "" for c in range(len(kl_im.instances) + install_count)}
|
||||||
|
name_scheme = init_name_scheme(kl_im.instances, install_count)
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
name_scheme = update_name_scheme(
|
||||||
|
name_scheme, name_dict, kl_im.instances, mr_im.instances
|
||||||
|
)
|
||||||
|
|
||||||
|
handle_instance_naming(name_dict, name_scheme)
|
||||||
|
|
||||||
|
create_example_cfg = get_confirm("Create example printer.cfg?")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not kl_im.instances:
|
||||||
|
check_install_dependencies(["git"])
|
||||||
|
setup_klipper_prerequesites()
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for name in name_dict:
|
||||||
|
if name_dict[name] in [n.suffix for n in kl_im.instances]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if check_is_single_to_multi_conversion(kl_im.instances):
|
||||||
|
handle_to_multi_instance_conversion(name_dict[name])
|
||||||
|
continue
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
create_klipper_instance(name_dict[name], create_example_cfg)
|
||||||
|
|
||||||
|
if count == install_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(e)
|
||||||
|
Logger.print_error("Klipper installation failed!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# step 4: check/handle conflicting packages/services
|
||||||
|
handle_disruptive_system_packages()
|
||||||
|
|
||||||
|
# step 5: check for required group membership
|
||||||
|
check_user_groups()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_klipper_prerequesites() -> None:
|
||||||
|
settings = KiauhSettings()
|
||||||
|
repo = settings.get("klipper", "repo_url")
|
||||||
|
branch = settings.get("klipper", "branch")
|
||||||
|
|
||||||
|
git_clone_wrapper(repo, KLIPPER_DIR, branch)
|
||||||
|
|
||||||
|
# install klipper dependencies and create python virtualenv
|
||||||
|
try:
|
||||||
|
install_klipper_packages(KLIPPER_DIR)
|
||||||
|
create_python_venv(KLIPPER_ENV_DIR)
|
||||||
|
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT)
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error("Error during installation of Klipper requirements!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def install_klipper_packages(klipper_dir: Path) -> None:
|
||||||
|
script = klipper_dir.joinpath("scripts/install-debian.sh")
|
||||||
|
packages = parse_packages_from_file(script)
|
||||||
|
packages = [pkg.replace("python-dev", "python3-dev") for pkg in packages]
|
||||||
|
packages.append("python3-venv")
|
||||||
|
# Add dfu-util for octopi-images
|
||||||
|
packages.append("dfu-util")
|
||||||
|
# Add dbus requirement for DietPi distro
|
||||||
|
if Path("/boot/dietpi/.version").exists():
|
||||||
|
packages.append("dbus")
|
||||||
|
|
||||||
|
check_install_dependencies(packages)
|
||||||
|
|
||||||
|
|
||||||
|
def update_klipper() -> None:
|
||||||
|
print_update_warn_dialog()
|
||||||
|
if not get_confirm("Update Klipper now?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = KiauhSettings()
|
||||||
|
if settings.get("kiauh", "backup_before_update"):
|
||||||
|
backup_klipper_dir()
|
||||||
|
|
||||||
|
instance_manager = InstanceManager(Klipper)
|
||||||
|
instance_manager.stop_all_instance()
|
||||||
|
|
||||||
|
git_pull_wrapper(repo=settings.get("klipper", "repo_url"), target_dir=KLIPPER_DIR)
|
||||||
|
|
||||||
|
# install possible new system packages
|
||||||
|
install_klipper_packages(KLIPPER_DIR)
|
||||||
|
# install possible new python dependencies
|
||||||
|
install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQUIREMENTS_TXT)
|
||||||
|
|
||||||
|
instance_manager.start_all_instance()
|
||||||
|
|
||||||
|
|
||||||
|
def create_klipper_instance(name: str, create_example_cfg: bool) -> None:
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
new_instance = Klipper(suffix=name)
|
||||||
|
kl_im.current_instance = new_instance
|
||||||
|
kl_im.create_instance()
|
||||||
|
kl_im.enable_instance()
|
||||||
|
if create_example_cfg:
|
||||||
|
# if a client-config is installed, include it in the new example cfg
|
||||||
|
clients = get_existing_clients()
|
||||||
|
create_example_printer_cfg(new_instance, clients)
|
||||||
|
kl_im.start_instance()
|
||||||
314
kiauh/components/klipper/klipper_utils.py
Normal file
314
kiauh/components/klipper/klipper_utils.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 grp
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from components.klipper import (
|
||||||
|
KLIPPER_BACKUP_DIR,
|
||||||
|
KLIPPER_DIR,
|
||||||
|
KLIPPER_ENV_DIR,
|
||||||
|
MODULE_PATH,
|
||||||
|
)
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper.klipper_dialogs import (
|
||||||
|
print_instance_overview,
|
||||||
|
print_missing_usergroup_dialog,
|
||||||
|
print_select_custom_name_dialog,
|
||||||
|
print_select_instance_count_dialog,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.moonraker_utils import moonraker_to_multi_conversion
|
||||||
|
from components.webui_client.base_data import BaseWebClient
|
||||||
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
|
create_client_config_symlink,
|
||||||
|
)
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.instance_manager.name_scheme import NameScheme
|
||||||
|
from utils import PRINTER_CFG_BACKUP_DIR
|
||||||
|
from utils.common import get_install_status
|
||||||
|
from utils.constants import CURRENT_USER
|
||||||
|
from utils.input_utils import get_confirm, get_number_input, get_string_input
|
||||||
|
from utils.logger import DialogType, Logger
|
||||||
|
from utils.sys_utils import cmd_sysctl_service
|
||||||
|
from utils.types import ComponentStatus
|
||||||
|
|
||||||
|
|
||||||
|
def get_klipper_status() -> ComponentStatus:
|
||||||
|
return get_install_status(KLIPPER_DIR, KLIPPER_ENV_DIR, Klipper)
|
||||||
|
|
||||||
|
|
||||||
|
def check_is_multi_install(
|
||||||
|
existing_instances: List[Klipper], install_count: int
|
||||||
|
) -> bool:
|
||||||
|
return not existing_instances and install_count > 1
|
||||||
|
|
||||||
|
|
||||||
|
def check_is_single_to_multi_conversion(
|
||||||
|
existing_instances: List[Klipper],
|
||||||
|
) -> bool:
|
||||||
|
return len(existing_instances) == 1 and existing_instances[0].suffix == ""
|
||||||
|
|
||||||
|
|
||||||
|
def init_name_scheme(
|
||||||
|
existing_instances: List[Klipper], install_count: int
|
||||||
|
) -> NameScheme:
|
||||||
|
if check_is_multi_install(
|
||||||
|
existing_instances, install_count
|
||||||
|
) or check_is_single_to_multi_conversion(existing_instances):
|
||||||
|
print_select_custom_name_dialog()
|
||||||
|
if get_confirm("Assign custom names?", False, allow_go_back=True):
|
||||||
|
return NameScheme.CUSTOM
|
||||||
|
else:
|
||||||
|
return NameScheme.INDEX
|
||||||
|
else:
|
||||||
|
return NameScheme.SINGLE
|
||||||
|
|
||||||
|
|
||||||
|
def update_name_scheme(
|
||||||
|
name_scheme: NameScheme,
|
||||||
|
name_dict: Dict[int, str],
|
||||||
|
klipper_instances: List[Klipper],
|
||||||
|
moonraker_instances: List[Moonraker],
|
||||||
|
) -> NameScheme:
|
||||||
|
# if there are more moonraker instances installed
|
||||||
|
# than klipper, we load their names into the name_dict,
|
||||||
|
# as we will detect and enforce that naming scheme
|
||||||
|
if len(moonraker_instances) > len(klipper_instances):
|
||||||
|
update_name_dict(name_dict, moonraker_instances)
|
||||||
|
return detect_name_scheme(moonraker_instances)
|
||||||
|
elif len(klipper_instances) > 1:
|
||||||
|
update_name_dict(name_dict, klipper_instances)
|
||||||
|
return detect_name_scheme(klipper_instances)
|
||||||
|
else:
|
||||||
|
return name_scheme
|
||||||
|
|
||||||
|
|
||||||
|
def update_name_dict(name_dict: Dict[int, str], instances: List[BaseInstance]) -> None:
|
||||||
|
for k, v in enumerate(instances):
|
||||||
|
name_dict[k] = v.suffix
|
||||||
|
|
||||||
|
|
||||||
|
def handle_instance_naming(name_dict: Dict[int, str], name_scheme: NameScheme) -> None:
|
||||||
|
if name_scheme == NameScheme.SINGLE:
|
||||||
|
return
|
||||||
|
|
||||||
|
for k in name_dict:
|
||||||
|
if name_dict[k] == "" and name_scheme == NameScheme.INDEX:
|
||||||
|
name_dict[k] = str(k + 1)
|
||||||
|
elif name_dict[k] == "" and name_scheme == NameScheme.CUSTOM:
|
||||||
|
assign_custom_name(k, name_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_existing() -> bool:
|
||||||
|
kl_instances = InstanceManager(Klipper).instances
|
||||||
|
print_instance_overview(kl_instances)
|
||||||
|
return get_confirm("Add new instances?", allow_go_back=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_install_count() -> Union[int, None]:
|
||||||
|
"""
|
||||||
|
Print a dialog for selecting the amount of Klipper instances
|
||||||
|
to set up with an option to navigate back. Returns None if the
|
||||||
|
user selected to go back, otherwise an integer greater or equal than 1 |
|
||||||
|
:return: Integer >= 1 or None
|
||||||
|
"""
|
||||||
|
kl_instances = InstanceManager(Klipper).instances
|
||||||
|
print_select_instance_count_dialog()
|
||||||
|
question = (
|
||||||
|
f"Number of"
|
||||||
|
f"{' additional' if len(kl_instances) > 0 else ''} "
|
||||||
|
f"Klipper instances to set up"
|
||||||
|
)
|
||||||
|
return get_number_input(question, 1, default=1, allow_go_back=True)
|
||||||
|
|
||||||
|
|
||||||
|
def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None:
|
||||||
|
existing_names = []
|
||||||
|
existing_names.extend(Klipper.blacklist())
|
||||||
|
existing_names.extend(name_dict[n] for n in name_dict)
|
||||||
|
question = f"Enter name for instance {key + 1}"
|
||||||
|
name_dict[key] = get_string_input(question, exclude=existing_names)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_to_multi_instance_conversion(new_name: str) -> None:
|
||||||
|
Logger.print_status("Converting single instance to multi instances ...")
|
||||||
|
klipper_to_multi_conversion(new_name)
|
||||||
|
moonraker_to_multi_conversion(new_name)
|
||||||
|
|
||||||
|
|
||||||
|
def klipper_to_multi_conversion(new_name: str) -> None:
|
||||||
|
Logger.print_status("Convert Klipper single to multi instance ...")
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
im.current_instance = im.instances[0]
|
||||||
|
|
||||||
|
# temporarily store the data dir path
|
||||||
|
old_data_dir = im.instances[0].data_dir
|
||||||
|
old_data_dir_name = im.instances[0].data_dir_name
|
||||||
|
|
||||||
|
# backup the old data_dir
|
||||||
|
bm = BackupManager()
|
||||||
|
name = f"config-{old_data_dir_name}"
|
||||||
|
bm.backup_directory(
|
||||||
|
name,
|
||||||
|
source=im.current_instance.cfg_dir,
|
||||||
|
target=PRINTER_CFG_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
|
# remove the old single instance
|
||||||
|
im.stop_instance()
|
||||||
|
im.disable_instance()
|
||||||
|
im.delete_instance()
|
||||||
|
|
||||||
|
# create a new klipper instance with the new name
|
||||||
|
new_instance = Klipper(suffix=new_name)
|
||||||
|
im.current_instance = new_instance
|
||||||
|
|
||||||
|
if not new_instance.data_dir.is_dir():
|
||||||
|
# rename the old data dir and use it for the new instance
|
||||||
|
Logger.print_status(f"Rename '{old_data_dir}' to '{new_instance.data_dir}' ...")
|
||||||
|
old_data_dir.rename(new_instance.data_dir)
|
||||||
|
else:
|
||||||
|
Logger.print_info(f"Existing '{new_instance.data_dir}' found ...")
|
||||||
|
|
||||||
|
# patch the virtual_sdcard sections path
|
||||||
|
# value to match the new printer_data foldername
|
||||||
|
cm = ConfigManager(new_instance.cfg_file)
|
||||||
|
if cm.config.has_section("virtual_sdcard"):
|
||||||
|
cm.set_value("virtual_sdcard", "path", str(new_instance.gcodes_dir))
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
# finalize creating the new instance
|
||||||
|
im.create_instance()
|
||||||
|
im.enable_instance()
|
||||||
|
im.start_instance()
|
||||||
|
|
||||||
|
|
||||||
|
def check_user_groups():
|
||||||
|
current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()]
|
||||||
|
|
||||||
|
missing_groups = []
|
||||||
|
if "tty" not in current_groups:
|
||||||
|
missing_groups.append("tty")
|
||||||
|
if "dialout" not in current_groups:
|
||||||
|
missing_groups.append("dialout")
|
||||||
|
|
||||||
|
if not missing_groups:
|
||||||
|
return
|
||||||
|
|
||||||
|
print_missing_usergroup_dialog(missing_groups)
|
||||||
|
if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"):
|
||||||
|
log = "Skipped adding user to required groups. You might encounter issues."
|
||||||
|
Logger.warn(log)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
for group in missing_groups:
|
||||||
|
Logger.print_status(f"Adding user '{CURRENT_USER}' to group {group} ...")
|
||||||
|
command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER]
|
||||||
|
run(command, check=True)
|
||||||
|
Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unable to add user to usergroups: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
log = "Remember to relog/restart this machine for the group(s) to be applied!"
|
||||||
|
Logger.print_warn(log)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_disruptive_system_packages() -> None:
|
||||||
|
services = []
|
||||||
|
|
||||||
|
command = ["systemctl", "is-enabled", "brltty"]
|
||||||
|
brltty_status = run(command, capture_output=True, text=True)
|
||||||
|
|
||||||
|
command = ["systemctl", "is-enabled", "brltty-udev"]
|
||||||
|
brltty_udev_status = run(command, capture_output=True, text=True)
|
||||||
|
|
||||||
|
command = ["systemctl", "is-enabled", "ModemManager"]
|
||||||
|
modem_manager_status = run(command, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if "enabled" in brltty_status.stdout:
|
||||||
|
services.append("brltty")
|
||||||
|
if "enabled" in brltty_udev_status.stdout:
|
||||||
|
services.append("brltty-udev")
|
||||||
|
if "enabled" in modem_manager_status.stdout:
|
||||||
|
services.append("ModemManager")
|
||||||
|
|
||||||
|
for service in services if services else []:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service(service, "mask")
|
||||||
|
except CalledProcessError:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.WARNING,
|
||||||
|
[
|
||||||
|
f"KIAUH was unable to mask the {service} system service. "
|
||||||
|
"Please fix the problem manually. Otherwise, this may have "
|
||||||
|
"undesirable effects on the operation of Klipper."
|
||||||
|
],
|
||||||
|
end="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_name_scheme(instance_list: List[BaseInstance]) -> NameScheme:
|
||||||
|
pattern = re.compile("^\d+$")
|
||||||
|
for instance in instance_list:
|
||||||
|
if not pattern.match(instance.suffix):
|
||||||
|
return NameScheme.CUSTOM
|
||||||
|
|
||||||
|
return NameScheme.INDEX
|
||||||
|
|
||||||
|
|
||||||
|
def get_highest_index(instance_list: List[Klipper]) -> int:
|
||||||
|
return max([int(instance.suffix.split("-")[-1]) for instance in instance_list])
|
||||||
|
|
||||||
|
|
||||||
|
def create_example_printer_cfg(
|
||||||
|
instance: Klipper, clients: Optional[List[BaseWebClient]] = None
|
||||||
|
) -> None:
|
||||||
|
Logger.print_status(f"Creating example printer.cfg in '{instance.cfg_dir}'")
|
||||||
|
if instance.cfg_file.is_file():
|
||||||
|
Logger.print_info(f"'{instance.cfg_file}' already exists.")
|
||||||
|
return
|
||||||
|
|
||||||
|
source = MODULE_PATH.joinpath("assets/printer.cfg")
|
||||||
|
target = instance.cfg_file
|
||||||
|
try:
|
||||||
|
shutil.copy(source, target)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to create example printer.cfg:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
cm = ConfigManager(target)
|
||||||
|
cm.set_value("virtual_sdcard", "path", str(instance.gcodes_dir))
|
||||||
|
|
||||||
|
# include existing client configs in the example config
|
||||||
|
if clients is not None and len(clients) > 0:
|
||||||
|
for c in clients:
|
||||||
|
client_config = c.client_config
|
||||||
|
section = client_config.config_section
|
||||||
|
cm.config.add_section(section=section)
|
||||||
|
create_client_config_symlink(client_config, [instance])
|
||||||
|
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
Logger.print_ok(f"Example printer.cfg created in '{instance.cfg_dir}'")
|
||||||
|
|
||||||
|
|
||||||
|
def backup_klipper_dir() -> None:
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory("klipper", source=KLIPPER_DIR, target=KLIPPER_BACKUP_DIR)
|
||||||
|
bm.backup_directory("klippy-env", source=KLIPPER_ENV_DIR, target=KLIPPER_BACKUP_DIR)
|
||||||
0
kiauh/components/klipper/menus/__init__.py
Normal file
0
kiauh/components/klipper/menus/__init__.py
Normal file
116
kiauh/components/klipper/menus/klipper_remove_menu.py
Normal file
116
kiauh/components/klipper/menus/klipper_remove_menu.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.klipper import klipper_remove
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
class KlipperRemoveMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.remove_klipper_service = False
|
||||||
|
self.remove_klipper_dir = False
|
||||||
|
self.remove_klipper_env = False
|
||||||
|
self.delete_klipper_logs = False
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else RemoveMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(method=self.toggle_all, menu=False),
|
||||||
|
"1": Option(method=self.toggle_remove_klipper_service, menu=False),
|
||||||
|
"2": Option(method=self.toggle_remove_klipper_dir, menu=False),
|
||||||
|
"3": Option(method=self.toggle_remove_klipper_env, menu=False),
|
||||||
|
"4": Option(method=self.toggle_delete_klipper_logs, menu=False),
|
||||||
|
"c": Option(method=self.run_removal_process, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ Remove Klipper ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
|
||||||
|
unchecked = "[ ]"
|
||||||
|
o1 = checked if self.remove_klipper_service else unchecked
|
||||||
|
o2 = checked if self.remove_klipper_dir else unchecked
|
||||||
|
o3 = checked if self.remove_klipper_env else unchecked
|
||||||
|
o4 = checked if self.delete_klipper_logs else unchecked
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Enter a number and hit enter to select / deselect |
|
||||||
|
| the specific option for removal. |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Select everything |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) {o1} Remove Service |
|
||||||
|
| 2) {o2} Remove Local Repository |
|
||||||
|
| 3) {o3} Remove Python Environment |
|
||||||
|
| 4) {o4} Delete all Log-Files |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| C) Continue |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def toggle_all(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_service = True
|
||||||
|
self.remove_klipper_dir = True
|
||||||
|
self.remove_klipper_env = True
|
||||||
|
self.delete_klipper_logs = True
|
||||||
|
|
||||||
|
def toggle_remove_klipper_service(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_service = not self.remove_klipper_service
|
||||||
|
|
||||||
|
def toggle_remove_klipper_dir(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_dir = not self.remove_klipper_dir
|
||||||
|
|
||||||
|
def toggle_remove_klipper_env(self, **kwargs) -> None:
|
||||||
|
self.remove_klipper_env = not self.remove_klipper_env
|
||||||
|
|
||||||
|
def toggle_delete_klipper_logs(self, **kwargs) -> None:
|
||||||
|
self.delete_klipper_logs = not self.delete_klipper_logs
|
||||||
|
|
||||||
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
|
if (
|
||||||
|
not self.remove_klipper_service
|
||||||
|
and not self.remove_klipper_dir
|
||||||
|
and not self.remove_klipper_env
|
||||||
|
and not self.delete_klipper_logs
|
||||||
|
):
|
||||||
|
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
klipper_remove.run_klipper_removal(
|
||||||
|
self.remove_klipper_service,
|
||||||
|
self.remove_klipper_dir,
|
||||||
|
self.remove_klipper_env,
|
||||||
|
self.delete_klipper_logs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.remove_klipper_service = False
|
||||||
|
self.remove_klipper_dir = False
|
||||||
|
self.remove_klipper_env = False
|
||||||
|
self.delete_klipper_logs = False
|
||||||
12
kiauh/components/klipper_firmware/__init__.py
Normal file
12
kiauh/components/klipper_firmware/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
|
||||||
|
SD_FLASH_SCRIPT = KLIPPER_DIR.joinpath("scripts/flash-sdcard.sh")
|
||||||
174
kiauh/components/klipper_firmware/firmware_utils.py
Normal file
174
kiauh/components/klipper_firmware/firmware_utils.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 subprocess import PIPE, STDOUT, CalledProcessError, Popen, check_output, run
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper_firmware import SD_FLASH_SCRIPT
|
||||||
|
from components.klipper_firmware.flash_options import (
|
||||||
|
FlashMethod,
|
||||||
|
FlashOptions,
|
||||||
|
)
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import log_process
|
||||||
|
|
||||||
|
|
||||||
|
def find_firmware_file() -> bool:
|
||||||
|
target = KLIPPER_DIR.joinpath("out")
|
||||||
|
target_exists = target.exists()
|
||||||
|
|
||||||
|
f1 = "klipper.elf.hex"
|
||||||
|
f2 = "klipper.elf"
|
||||||
|
f3 = "klipper.bin"
|
||||||
|
fw_file_exists = (
|
||||||
|
target.joinpath(f1).exists() and target.joinpath(f2).exists()
|
||||||
|
) or target.joinpath(f3).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)
|
||||||
|
return output.splitlines()
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error("Unable to find a USB device!")
|
||||||
|
Logger.print_error(e, prefix=False)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error("Unable to find a UART device!")
|
||||||
|
Logger.print_error(e, prefix=False)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error("Unable to find a USB DFU 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 []
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = f"{SD_FLASH_SCRIPT} -l"
|
||||||
|
blist = check_output(cmd, shell=True, text=True)
|
||||||
|
return blist.splitlines()[1:]
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"An unexpected error occured:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def start_flash_process(flash_options: FlashOptions) -> None:
|
||||||
|
Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...")
|
||||||
|
try:
|
||||||
|
if not flash_options.flash_method:
|
||||||
|
raise Exception("Missing value for flash_method!")
|
||||||
|
if not flash_options.flash_command:
|
||||||
|
raise Exception("Missing value for flash_command!")
|
||||||
|
if not flash_options.selected_mcu:
|
||||||
|
raise Exception("Missing value for selected_mcu!")
|
||||||
|
if not flash_options.connection_type:
|
||||||
|
raise Exception("Missing value for connection_type!")
|
||||||
|
if (
|
||||||
|
flash_options.flash_method == FlashMethod.SD_CARD
|
||||||
|
and not flash_options.selected_board
|
||||||
|
):
|
||||||
|
raise Exception("Missing value for selected_board!")
|
||||||
|
|
||||||
|
if flash_options.flash_method is FlashMethod.REGULAR:
|
||||||
|
cmd = [
|
||||||
|
"make",
|
||||||
|
flash_options.flash_command.value,
|
||||||
|
f"FLASH_DEVICE={flash_options.selected_mcu}",
|
||||||
|
]
|
||||||
|
elif flash_options.flash_method is FlashMethod.SD_CARD:
|
||||||
|
if not SD_FLASH_SCRIPT.exists():
|
||||||
|
raise Exception("Unable to find Klippers sdcard flash script!")
|
||||||
|
cmd = [
|
||||||
|
SD_FLASH_SCRIPT.as_posix(),
|
||||||
|
f"-b {flash_options.selected_baudrate}",
|
||||||
|
flash_options.selected_mcu,
|
||||||
|
flash_options.selected_board,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid value for flash_method!")
|
||||||
|
|
||||||
|
instance_manager = InstanceManager(Klipper)
|
||||||
|
instance_manager.stop_all_instance()
|
||||||
|
|
||||||
|
process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True)
|
||||||
|
log_process(process)
|
||||||
|
|
||||||
|
instance_manager.start_all_instance()
|
||||||
|
|
||||||
|
rc = process.returncode
|
||||||
|
if rc != 0:
|
||||||
|
raise Exception(f"Flashing failed with returncode: {rc}")
|
||||||
|
else:
|
||||||
|
Logger.print_ok("Flashing successfull!", start="\n", end="\n\n")
|
||||||
|
|
||||||
|
except (Exception, CalledProcessError):
|
||||||
|
Logger.print_error("Flashing failed!", start="\n")
|
||||||
|
Logger.print_error("See the console output above!", end="\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def run_make_clean() -> None:
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make clean",
|
||||||
|
cwd=KLIPPER_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unexpected error:\n{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def run_make_menuconfig() -> None:
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make PYTHON=python3 menuconfig",
|
||||||
|
cwd=KLIPPER_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unexpected error:\n{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def run_make() -> None:
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
"make PYTHON=python3",
|
||||||
|
cwd=KLIPPER_DIR,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Unexpected error:\n{e}")
|
||||||
|
raise
|
||||||
104
kiauh/components/klipper_firmware/flash_options.py
Normal file
104
kiauh/components/klipper_firmware/flash_options.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 dataclasses import field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
|
||||||
|
class FlashMethod(Enum):
|
||||||
|
REGULAR = "Regular"
|
||||||
|
SD_CARD = "SD Card"
|
||||||
|
|
||||||
|
|
||||||
|
class FlashCommand(Enum):
|
||||||
|
FLASH = "flash"
|
||||||
|
SERIAL_FLASH = "serialflash"
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionType(Enum):
|
||||||
|
USB = "USB"
|
||||||
|
USB_DFU = "USB (DFU)"
|
||||||
|
UART = "UART"
|
||||||
|
|
||||||
|
|
||||||
|
class FlashOptions:
|
||||||
|
_instance = None
|
||||||
|
_flash_method: Union[FlashMethod, None] = None
|
||||||
|
_flash_command: Union[FlashCommand, None] = None
|
||||||
|
_connection_type: Union[ConnectionType, None] = None
|
||||||
|
_mcu_list: List[str] = field(default_factory=list)
|
||||||
|
_selected_mcu: str = ""
|
||||||
|
_selected_board: str = ""
|
||||||
|
_selected_baudrate: int = 250000
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if not cls._instance:
|
||||||
|
cls._instance = super(FlashOptions, cls).__new__(cls, *args, **kwargs)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def destroy(cls):
|
||||||
|
cls._instance = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flash_method(self) -> Union[FlashMethod, None]:
|
||||||
|
return self._flash_method
|
||||||
|
|
||||||
|
@flash_method.setter
|
||||||
|
def flash_method(self, value: Union[FlashMethod, None]):
|
||||||
|
self._flash_method = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flash_command(self) -> Union[FlashCommand, None]:
|
||||||
|
return self._flash_command
|
||||||
|
|
||||||
|
@flash_command.setter
|
||||||
|
def flash_command(self, value: Union[FlashCommand, None]):
|
||||||
|
self._flash_command = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection_type(self) -> Union[ConnectionType, None]:
|
||||||
|
return self._connection_type
|
||||||
|
|
||||||
|
@connection_type.setter
|
||||||
|
def connection_type(self, value: Union[ConnectionType, None]):
|
||||||
|
self._connection_type = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mcu_list(self) -> List[str]:
|
||||||
|
return self._mcu_list
|
||||||
|
|
||||||
|
@mcu_list.setter
|
||||||
|
def mcu_list(self, value: List[str]) -> None:
|
||||||
|
self._mcu_list = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected_mcu(self) -> str:
|
||||||
|
return self._selected_mcu
|
||||||
|
|
||||||
|
@selected_mcu.setter
|
||||||
|
def selected_mcu(self, value: str) -> None:
|
||||||
|
self._selected_mcu = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected_board(self) -> str:
|
||||||
|
return self._selected_board
|
||||||
|
|
||||||
|
@selected_board.setter
|
||||||
|
def selected_board(self, value: str) -> None:
|
||||||
|
self._selected_board = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected_baudrate(self) -> int:
|
||||||
|
return self._selected_baudrate
|
||||||
|
|
||||||
|
@selected_baudrate.setter
|
||||||
|
def selected_baudrate(self, value: int) -> None:
|
||||||
|
self._selected_baudrate = value
|
||||||
112
kiauh/components/klipper_firmware/menus/klipper_build_menu.py
Normal file
112
kiauh/components/klipper_firmware/menus/klipper_build_menu.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
from components.klipper_firmware.firmware_utils import (
|
||||||
|
run_make,
|
||||||
|
run_make_clean,
|
||||||
|
run_make_menuconfig,
|
||||||
|
)
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_GREEN, COLOR_RED, RESET_FORMAT
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
check_package_install,
|
||||||
|
install_system_packages,
|
||||||
|
update_system_package_lists,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperBuildFirmwareMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.deps = ["build-essential", "dpkg-dev", "make"]
|
||||||
|
self.missing_deps = check_package_install(self.deps)
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else AdvancedMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
if len(self.missing_deps) == 0:
|
||||||
|
self.input_label_txt = "Press ENTER to continue"
|
||||||
|
self.default_option = Option(method=self.start_build_process, menu=False)
|
||||||
|
else:
|
||||||
|
self.input_label_txt = "Press ENTER to install dependencies"
|
||||||
|
self.default_option = Option(method=self.install_missing_deps, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ Build Firmware Menu ] "
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| The following dependencies are required: |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for d in self.deps:
|
||||||
|
status_ok = f"{COLOR_GREEN}*INSTALLED*{RESET_FORMAT}"
|
||||||
|
status_missing = f"{COLOR_RED}*MISSING*{RESET_FORMAT}"
|
||||||
|
status = status_missing if d in self.missing_deps else status_ok
|
||||||
|
padding = 39 - len(d) + len(status) + (len(status_ok) - len(status))
|
||||||
|
d = f" {COLOR_CYAN}● {d}{RESET_FORMAT}"
|
||||||
|
menu += f"| {d}{status:>{padding}} |\n"
|
||||||
|
|
||||||
|
menu += "| |\n"
|
||||||
|
if len(self.missing_deps) == 0:
|
||||||
|
line = f"{COLOR_GREEN}All dependencies are met!{RESET_FORMAT}"
|
||||||
|
else:
|
||||||
|
line = f"{COLOR_RED}Dependencies are missing!{RESET_FORMAT}"
|
||||||
|
|
||||||
|
menu += f"| {line:<62} |\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def install_missing_deps(self, **kwargs) -> None:
|
||||||
|
try:
|
||||||
|
update_system_package_lists(silent=False)
|
||||||
|
Logger.print_status("Installing system packages...")
|
||||||
|
install_system_packages(self.missing_deps)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(e)
|
||||||
|
Logger.print_error("Installing dependencies failed!")
|
||||||
|
finally:
|
||||||
|
# restart this menu
|
||||||
|
KlipperBuildFirmwareMenu().run()
|
||||||
|
|
||||||
|
def start_build_process(self, **kwargs) -> None:
|
||||||
|
try:
|
||||||
|
run_make_clean()
|
||||||
|
run_make_menuconfig()
|
||||||
|
run_make()
|
||||||
|
|
||||||
|
Logger.print_ok("Firmware successfully built!")
|
||||||
|
Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(e)
|
||||||
|
Logger.print_error("Building Klipper Firmware failed!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.previous_menu().run()
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.klipper_firmware.flash_options import FlashMethod, FlashOptions
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_RED, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperNoFirmwareErrorMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.footer_type = FooterType.BLANK
|
||||||
|
self.input_label_txt = "Press ENTER to go back to [Advanced Menu]"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.default_option = Option(self.go_back, False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! NO FIRMWARE FILE FOUND !!!"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
line1 = f"{color}Unable to find a compiled firmware file!{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:<62} |
|
||||||
|
| |
|
||||||
|
| Make sure, that: |
|
||||||
|
| ● the folder '~/klipper/out' and its content exist |
|
||||||
|
| ● the folder contains the following file: |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if self.flash_options.flash_method is FlashMethod.REGULAR:
|
||||||
|
menu += "| ● 'klipper.elf' |\n"
|
||||||
|
menu += "| ● 'klipper.elf.hex' |\n"
|
||||||
|
else:
|
||||||
|
menu += "| ● 'klipper.bin' |\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def go_back(self, **kwargs) -> None:
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
AdvancedMenu().run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperNoBoardTypesErrorMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.footer_type = FooterType.BLANK
|
||||||
|
self.input_label_txt = "Press ENTER to go back to [Main Menu]"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.default_option = Option(self.go_back, False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! ERROR GETTING BOARD LIST !!!"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
line1 = f"{color}Reading the list of supported boards failed!{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:<62} |
|
||||||
|
| |
|
||||||
|
| Make sure, that: |
|
||||||
|
| ● the folder '~/klipper' and all its content exist |
|
||||||
|
| ● the content of folder '~/klipper' is not currupted |
|
||||||
|
| ● the file '~/klipper/scripts/flash-sd.py' exist |
|
||||||
|
| ● your current user has access to those files/folders |
|
||||||
|
| |
|
||||||
|
| If in doubt or this process continues to fail, please |
|
||||||
|
| consider to download Klipper again. |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def go_back(self, **kwargs) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
MainMenu().run()
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection DuplicatedCode
|
||||||
|
class KlipperFlashMethodHelpMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperFlashMethodMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashMethodMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " < ? > Help: Flash MCU < ? > "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
subheader1 = f"{COLOR_CYAN}Regular flashing method:{RESET_FORMAT}"
|
||||||
|
subheader2 = f"{COLOR_CYAN}Updating via SD-Card Update:{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {subheader1:<62} |
|
||||||
|
| The default method to flash controller boards which |
|
||||||
|
| are connected and updated over USB and not by placing |
|
||||||
|
| a compiled firmware file onto an internal SD-Card. |
|
||||||
|
| |
|
||||||
|
| Common controllers that get flashed that way are: |
|
||||||
|
| - Arduino Mega 2560 |
|
||||||
|
| - Fysetc F6 / S6 (used without a Display + SD-Slot) |
|
||||||
|
| |
|
||||||
|
| {subheader2:<62} |
|
||||||
|
| Many popular controller boards ship with a bootloader |
|
||||||
|
| capable of updating the firmware via SD-Card. |
|
||||||
|
| Choose this method if your controller board supports |
|
||||||
|
| this way of updating. This method ONLY works for up- |
|
||||||
|
| grading firmware. The initial flashing procedure must |
|
||||||
|
| be done manually per the instructions that apply to |
|
||||||
|
| your controller board. |
|
||||||
|
| |
|
||||||
|
| Common controllers that can be flashed that way are: |
|
||||||
|
| - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 |
|
||||||
|
| - Fysetc F6 / S6 (used with a Display + SD-Slot) |
|
||||||
|
| - Fysetc Spider |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection DuplicatedCode
|
||||||
|
class KlipperFlashCommandHelpMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperFlashCommandMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashCommandMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " < ? > Help: Flash MCU < ? > "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
subheader1 = f"{COLOR_CYAN}make flash:{RESET_FORMAT}"
|
||||||
|
subheader2 = f"{COLOR_CYAN}make serialflash:{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {subheader1:<62} |
|
||||||
|
| The default command to flash controller board, it |
|
||||||
|
| will detect selected microcontroller and use suitable |
|
||||||
|
| tool for flashing it. |
|
||||||
|
| |
|
||||||
|
| {subheader2:<62} |
|
||||||
|
| Special command to flash STM32 microcontrollers in |
|
||||||
|
| DFU mode but connected via serial. stm32flash command |
|
||||||
|
| will be used internally. |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection DuplicatedCode
|
||||||
|
class KlipperMcuConnectionHelpMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperSelectMcuConnectionMenu,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu
|
||||||
|
if previous_menu is not None
|
||||||
|
else KlipperSelectMcuConnectionMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " < ? > Help: Flash MCU < ? > "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
subheader1 = f"{COLOR_CYAN}USB:{RESET_FORMAT}"
|
||||||
|
subheader2 = f"{COLOR_CYAN}UART:{RESET_FORMAT}"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {subheader1:<62} |
|
||||||
|
| Selecting USB as the connection method will scan the |
|
||||||
|
| USB ports for connected controller boards. This will |
|
||||||
|
| be similar to the 'ls /dev/serial/by-id/*' command |
|
||||||
|
| suggested by the official Klipper documentation for |
|
||||||
|
| determining successfull USB connections! |
|
||||||
|
| |
|
||||||
|
| {subheader2:<62} |
|
||||||
|
| Selecting UART as the connection method will list all |
|
||||||
|
| possible UART serial ports. Note: This method ALWAYS |
|
||||||
|
| returns something as it seems impossible to determine |
|
||||||
|
| if a valid Klipper controller board is connected or |
|
||||||
|
| not. Because of that, you MUST know which UART serial |
|
||||||
|
| port your controller board is connected to when using |
|
||||||
|
| this connection method. |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
442
kiauh/components/klipper_firmware/menus/klipper_flash_menu.py
Normal file
442
kiauh/components/klipper_firmware/menus/klipper_flash_menu.py
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
import time
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.klipper_firmware.firmware_utils import (
|
||||||
|
find_firmware_file,
|
||||||
|
find_uart_device,
|
||||||
|
find_usb_device_by_id,
|
||||||
|
find_usb_dfu_device,
|
||||||
|
get_sd_flash_board_list,
|
||||||
|
start_flash_process,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.flash_options import (
|
||||||
|
ConnectionType,
|
||||||
|
FlashCommand,
|
||||||
|
FlashMethod,
|
||||||
|
FlashOptions,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_error_menu import (
|
||||||
|
KlipperNoBoardTypesErrorMenu,
|
||||||
|
KlipperNoFirmwareErrorMenu,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_help_menu import (
|
||||||
|
KlipperFlashCommandHelpMenu,
|
||||||
|
KlipperFlashMethodHelpMenu,
|
||||||
|
KlipperMcuConnectionHelpMenu,
|
||||||
|
)
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_RED, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
from utils.input_utils import get_number_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperFlashMethodMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.help_menu = KlipperFlashMethodHelpMenu
|
||||||
|
self.input_label_txt = "Select flash method"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else AdvancedMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(self.select_regular, menu=False),
|
||||||
|
"2": Option(self.select_sdcard, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ MCU Flash Menu ] "
|
||||||
|
subheader = f"{COLOR_YELLOW}ATTENTION:{RESET_FORMAT}"
|
||||||
|
subline1 = f"{COLOR_YELLOW}Make sure to select the correct method for the MCU!{RESET_FORMAT}"
|
||||||
|
subline2 = f"{COLOR_YELLOW}Not all MCUs support both methods!{RESET_FORMAT}"
|
||||||
|
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Select the flash method for flashing the MCU. |
|
||||||
|
| |
|
||||||
|
| {subheader:<62} |
|
||||||
|
| {subline1:<62} |
|
||||||
|
| {subline2:<62} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| |
|
||||||
|
| 1) Regular flashing method |
|
||||||
|
| 2) Updating via SD-Card Update |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def select_regular(self, **kwargs):
|
||||||
|
self.flash_options.flash_method = FlashMethod.REGULAR
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def select_sdcard(self, **kwargs):
|
||||||
|
self.flash_options.flash_method = FlashMethod.SD_CARD
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def goto_next_menu(self, **kwargs):
|
||||||
|
if find_firmware_file():
|
||||||
|
KlipperFlashCommandMenu(previous_menu=self.__class__).run()
|
||||||
|
else:
|
||||||
|
KlipperNoFirmwareErrorMenu().run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperFlashCommandMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.help_menu = KlipperFlashCommandHelpMenu
|
||||||
|
self.input_label_txt = "Select flash command"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashMethodMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(self.select_flash, menu=False),
|
||||||
|
"2": Option(self.select_serialflash, menu=False),
|
||||||
|
}
|
||||||
|
self.default_option = Option(self.select_flash, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
/=======================================================\\
|
||||||
|
| |
|
||||||
|
| Which flash command to use for flashing the MCU? |
|
||||||
|
| 1) make flash (default) |
|
||||||
|
| 2) make serialflash (stm32flash) |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def select_flash(self, **kwargs):
|
||||||
|
self.flash_options.flash_command = FlashCommand.FLASH
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def select_serialflash(self, **kwargs):
|
||||||
|
self.flash_options.flash_command = FlashCommand.SERIAL_FLASH
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def goto_next_menu(self, **kwargs):
|
||||||
|
KlipperSelectMcuConnectionMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperSelectMcuConnectionMenu(BaseMenu):
|
||||||
|
def __init__(
|
||||||
|
self, previous_menu: Optional[Type[BaseMenu]] = None, standalone: bool = False
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.__standalone = standalone
|
||||||
|
self.help_menu = KlipperMcuConnectionHelpMenu
|
||||||
|
self.input_label_txt = "Select connection type"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperFlashCommandMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.select_usb, menu=False),
|
||||||
|
"2": Option(method=self.select_dfu, menu=False),
|
||||||
|
"3": Option(method=self.select_usb_dfu, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "Make sure that the controller board is connected now!"
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| |
|
||||||
|
| How is the controller board connected to the host? |
|
||||||
|
| 1) USB |
|
||||||
|
| 2) UART |
|
||||||
|
| 3) USB (DFU mode) |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def select_usb(self, **kwargs):
|
||||||
|
self.flash_options.connection_type = ConnectionType.USB
|
||||||
|
self.get_mcu_list()
|
||||||
|
|
||||||
|
def select_dfu(self, **kwargs):
|
||||||
|
self.flash_options.connection_type = ConnectionType.UART
|
||||||
|
self.get_mcu_list()
|
||||||
|
|
||||||
|
def select_usb_dfu(self, **kwargs):
|
||||||
|
self.flash_options.connection_type = ConnectionType.USB_DFU
|
||||||
|
self.get_mcu_list()
|
||||||
|
|
||||||
|
def get_mcu_list(self, **kwargs):
|
||||||
|
conn_type = self.flash_options.connection_type
|
||||||
|
|
||||||
|
if conn_type is ConnectionType.USB:
|
||||||
|
Logger.print_status("Identifying MCU connected via USB ...")
|
||||||
|
self.flash_options.mcu_list = find_usb_device_by_id()
|
||||||
|
elif conn_type is ConnectionType.UART:
|
||||||
|
Logger.print_status("Identifying MCU possibly connected via UART ...")
|
||||||
|
self.flash_options.mcu_list = find_uart_device()
|
||||||
|
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()
|
||||||
|
|
||||||
|
if len(self.flash_options.mcu_list) < 1:
|
||||||
|
Logger.print_warn("No MCUs found!")
|
||||||
|
Logger.print_warn("Make sure they are connected and repeat this step.")
|
||||||
|
|
||||||
|
# if standalone is True, we only display the MCUs to the user and return
|
||||||
|
if self.__standalone and len(self.flash_options.mcu_list) > 0:
|
||||||
|
Logger.print_ok("The following MCUs were found:", prefix=False)
|
||||||
|
for i, mcu in enumerate(self.flash_options.mcu_list):
|
||||||
|
print(f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}")
|
||||||
|
time.sleep(3)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.goto_next_menu()
|
||||||
|
|
||||||
|
def goto_next_menu(self, **kwargs):
|
||||||
|
KlipperSelectMcuIdMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperSelectMcuIdMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.mcu_list = self.flash_options.mcu_list
|
||||||
|
self.input_label_txt = "Select MCU to flash"
|
||||||
|
self.footer_type = FooterType.BACK_HELP
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu
|
||||||
|
if previous_menu is not None
|
||||||
|
else KlipperSelectMcuConnectionMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
f"{i}": Option(self.flash_mcu, False, f"{i}")
|
||||||
|
for i in range(len(self.mcu_list))
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! ATTENTION !!!"
|
||||||
|
header2 = f"[{COLOR_CYAN}List of available MCUs{RESET_FORMAT}]"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Make sure, to select the correct MCU! |
|
||||||
|
| ONLY flash a firmware created for the respective MCU! |
|
||||||
|
| |
|
||||||
|
|{header2:-^64}|
|
||||||
|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for i, mcu in enumerate(self.mcu_list):
|
||||||
|
mcu = mcu.split("/")[-1]
|
||||||
|
menu += f" ● MCU #{i}: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n"
|
||||||
|
|
||||||
|
print(menu, end="\n")
|
||||||
|
|
||||||
|
def flash_mcu(self, **kwargs):
|
||||||
|
index = int(kwargs.get("opt_index"))
|
||||||
|
selected_mcu = self.mcu_list[index]
|
||||||
|
self.flash_options.selected_mcu = selected_mcu
|
||||||
|
|
||||||
|
if self.flash_options.flash_method == FlashMethod.SD_CARD:
|
||||||
|
KlipperSelectSDFlashBoardMenu(previous_menu=self.__class__).run()
|
||||||
|
elif self.flash_options.flash_method == FlashMethod.REGULAR:
|
||||||
|
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperSelectSDFlashBoardMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.available_boards = get_sd_flash_board_list()
|
||||||
|
self.input_label_txt = "Select board type"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else KlipperSelectMcuIdMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
f"{i}": Option(self.board_select, False, f"{i}")
|
||||||
|
for i in range(len(self.available_boards))
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
if len(self.available_boards) < 1:
|
||||||
|
KlipperNoBoardTypesErrorMenu().run()
|
||||||
|
else:
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
/=======================================================\\
|
||||||
|
| Please select the type of board that corresponds to |
|
||||||
|
| the currently selected MCU ID you chose before. |
|
||||||
|
| |
|
||||||
|
| The following boards are currently supported: |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for i, board in enumerate(self.available_boards):
|
||||||
|
line = f" {i}) {board}"
|
||||||
|
menu += f"|{line:<55}|\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def board_select(self, **kwargs):
|
||||||
|
board = int(kwargs.get("opt_index"))
|
||||||
|
self.flash_options.selected_board = self.available_boards[board]
|
||||||
|
self.baudrate_select()
|
||||||
|
|
||||||
|
def baudrate_select(self, **kwargs):
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
/=======================================================\\
|
||||||
|
| If your board is flashed with firmware that connects |
|
||||||
|
| at a custom baud rate, please change it now. |
|
||||||
|
| |
|
||||||
|
| If you are unsure, stick to the default 250000! |
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
self.flash_options.selected_baudrate = get_number_input(
|
||||||
|
question="Please set the baud rate",
|
||||||
|
default=250000,
|
||||||
|
min_count=0,
|
||||||
|
allow_go_back=True,
|
||||||
|
)
|
||||||
|
KlipperFlashOverviewMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperFlashOverviewMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.flash_options = FlashOptions()
|
||||||
|
self.input_label_txt = "Perform action (default=Y)"
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = previous_menu
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"Y": Option(self.execute_flash, menu=False),
|
||||||
|
"N": Option(self.abort_process, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.default_option = Option(self.execute_flash, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = "!!! ATTENTION !!!"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
|
||||||
|
method = self.flash_options.flash_method.value
|
||||||
|
command = self.flash_options.flash_command.value
|
||||||
|
conn_type = self.flash_options.connection_type.value
|
||||||
|
mcu = self.flash_options.selected_mcu
|
||||||
|
board = self.flash_options.selected_board
|
||||||
|
baudrate = self.flash_options.selected_baudrate
|
||||||
|
subheader = f"[{COLOR_CYAN}Overview{RESET_FORMAT}]"
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Before contuining the flashing process, please check |
|
||||||
|
| if all parameters were set correctly! Once you made |
|
||||||
|
| sure everything is correct, start the process. If any |
|
||||||
|
| parameter needs to be changed, you can go back (B) |
|
||||||
|
| step by step or abort and start from the beginning. |
|
||||||
|
|{subheader:-^64}|
|
||||||
|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
menu += f" ● MCU: {COLOR_CYAN}{mcu}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Connection: {COLOR_CYAN}{conn_type}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Flash method: {COLOR_CYAN}{method}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Flash command: {COLOR_CYAN}{command}{RESET_FORMAT}\n"
|
||||||
|
|
||||||
|
if self.flash_options.flash_method is FlashMethod.SD_CARD:
|
||||||
|
menu += f" ● Board type: {COLOR_CYAN}{board}{RESET_FORMAT}\n"
|
||||||
|
menu += f" ● Baudrate: {COLOR_CYAN}{baudrate}{RESET_FORMAT}\n"
|
||||||
|
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Y) Start flash process |
|
||||||
|
| N) Abort - Return to Advanced Menu |
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def execute_flash(self, **kwargs):
|
||||||
|
start_flash_process(self.flash_options)
|
||||||
|
Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...")
|
||||||
|
time.sleep(5)
|
||||||
|
KlipperFlashMethodMenu().run()
|
||||||
|
|
||||||
|
def abort_process(self, **kwargs):
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
|
||||||
|
AdvancedMenu().run()
|
||||||
16
kiauh/components/klipperscreen/__init__.py
Normal file
16
kiauh/components/klipperscreen/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git"
|
||||||
|
KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen")
|
||||||
|
KLIPPERSCREEN_ENV = Path.home().joinpath(".KlipperScreen-env")
|
||||||
|
KLIPPERSCREEN_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipperscreen-backups")
|
||||||
219
kiauh/components/klipperscreen/klipperscreen.py
Normal file
219
kiauh/components/klipperscreen/klipperscreen.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipperscreen import (
|
||||||
|
KLIPPERSCREEN_BACKUP_DIR,
|
||||||
|
KLIPPERSCREEN_DIR,
|
||||||
|
KLIPPERSCREEN_ENV,
|
||||||
|
KLIPPERSCREEN_REPO,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.common import (
|
||||||
|
check_install_dependencies,
|
||||||
|
get_install_status,
|
||||||
|
)
|
||||||
|
from utils.config_utils import add_config_section, remove_config_section
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.fs_utils import remove_with_sudo
|
||||||
|
from utils.git_utils import (
|
||||||
|
git_clone_wrapper,
|
||||||
|
git_pull_wrapper,
|
||||||
|
)
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import DialogType, Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
check_python_version,
|
||||||
|
cmd_sysctl_manage,
|
||||||
|
cmd_sysctl_service,
|
||||||
|
install_python_requirements,
|
||||||
|
)
|
||||||
|
from utils.types import ComponentStatus
|
||||||
|
|
||||||
|
|
||||||
|
def install_klipperscreen() -> None:
|
||||||
|
Logger.print_status("Installing KlipperScreen ...")
|
||||||
|
|
||||||
|
if not check_python_version(3, 7):
|
||||||
|
return
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances = mr_im.instances
|
||||||
|
if not mr_instances:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.WARNING,
|
||||||
|
[
|
||||||
|
"Moonraker not found! KlipperScreen will not properly work "
|
||||||
|
"without a working Moonraker installation.",
|
||||||
|
"\n\n",
|
||||||
|
"KlipperScreens update manager configuration for Moonraker "
|
||||||
|
"will not be added to any moonraker.conf.",
|
||||||
|
],
|
||||||
|
end="",
|
||||||
|
)
|
||||||
|
if not get_confirm(
|
||||||
|
"Continue KlipperScreen installation?",
|
||||||
|
default_choice=False,
|
||||||
|
allow_go_back=True,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
package_list = ["git", "wget", "curl", "unzip", "dfu-util"]
|
||||||
|
check_install_dependencies(package_list)
|
||||||
|
|
||||||
|
git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR)
|
||||||
|
|
||||||
|
try:
|
||||||
|
script = f"{KLIPPERSCREEN_DIR}/scripts/KlipperScreen-install.sh"
|
||||||
|
run(script, shell=True, check=True)
|
||||||
|
if mr_instances:
|
||||||
|
patch_klipperscreen_update_manager(mr_instances)
|
||||||
|
mr_im.restart_all_instance()
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
"Moonraker is not installed! Cannot add "
|
||||||
|
"KlipperScreen to update manager!"
|
||||||
|
)
|
||||||
|
Logger.print_ok("KlipperScreen successfully installed!")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error installing KlipperScreen:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None:
|
||||||
|
env_py = f"{KLIPPERSCREEN_ENV}/bin/python"
|
||||||
|
add_config_section(
|
||||||
|
section="update_manager KlipperScreen",
|
||||||
|
instances=instances,
|
||||||
|
options=[
|
||||||
|
("type", "git_repo"),
|
||||||
|
("path", str(KLIPPERSCREEN_DIR)),
|
||||||
|
("orgin", KLIPPERSCREEN_REPO),
|
||||||
|
("env", env_py),
|
||||||
|
("requirements", "scripts/KlipperScreen-requirements.txt"),
|
||||||
|
("install_script", "scripts/KlipperScreen-install.sh"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_klipperscreen() -> None:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service("KlipperScreen", "stop")
|
||||||
|
|
||||||
|
if not KLIPPERSCREEN_DIR.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
"KlipperScreen does not seem to be installed! Skipping ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Updating KlipperScreen ...")
|
||||||
|
|
||||||
|
cmd_sysctl_service("KlipperScreen", "stop")
|
||||||
|
|
||||||
|
settings = KiauhSettings()
|
||||||
|
if settings.get("kiauh", "backup_before_update"):
|
||||||
|
backup_klipperscreen_dir()
|
||||||
|
|
||||||
|
git_pull_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR)
|
||||||
|
|
||||||
|
requirements = KLIPPERSCREEN_DIR.joinpath(
|
||||||
|
"/scripts/KlipperScreen-requirements.txt"
|
||||||
|
)
|
||||||
|
install_python_requirements(KLIPPERSCREEN_ENV, requirements)
|
||||||
|
|
||||||
|
cmd_sysctl_service("KlipperScreen", "start")
|
||||||
|
|
||||||
|
Logger.print_ok("KlipperScreen updated successfully.", end="\n\n")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error updating KlipperScreen:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def get_klipperscreen_status() -> ComponentStatus:
|
||||||
|
return get_install_status(
|
||||||
|
KLIPPERSCREEN_DIR,
|
||||||
|
KLIPPERSCREEN_ENV,
|
||||||
|
files=[SYSTEMD.joinpath("KlipperScreen.service")],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_klipperscreen() -> None:
|
||||||
|
Logger.print_status("Removing KlipperScreen ...")
|
||||||
|
try:
|
||||||
|
if KLIPPERSCREEN_DIR.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen directory ...")
|
||||||
|
shutil.rmtree(KLIPPERSCREEN_DIR)
|
||||||
|
Logger.print_ok("KlipperScreen directory successfully removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_warn("KlipperScreen directory not found!")
|
||||||
|
|
||||||
|
if KLIPPERSCREEN_ENV.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen environment ...")
|
||||||
|
shutil.rmtree(KLIPPERSCREEN_ENV)
|
||||||
|
Logger.print_ok("KlipperScreen environment successfully removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_warn("KlipperScreen environment not found!")
|
||||||
|
|
||||||
|
service = SYSTEMD.joinpath("KlipperScreen.service")
|
||||||
|
if service.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen service ...")
|
||||||
|
cmd_sysctl_service(service, "stop")
|
||||||
|
cmd_sysctl_service(service, "disable")
|
||||||
|
remove_with_sudo(service)
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
cmd_sysctl_manage("reset-failed")
|
||||||
|
Logger.print_ok("KlipperScreen service successfully removed!")
|
||||||
|
|
||||||
|
logfile = Path("/tmp/KlipperScreen.log")
|
||||||
|
if logfile.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen log file ...")
|
||||||
|
remove_with_sudo(logfile)
|
||||||
|
Logger.print_ok("KlipperScreen log file successfully removed!")
|
||||||
|
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances: List[Klipper] = kl_im.instances
|
||||||
|
for instance in kl_instances:
|
||||||
|
logfile = instance.log_dir.joinpath("KlipperScreen.log")
|
||||||
|
if logfile.exists():
|
||||||
|
Logger.print_status(f"Removing {logfile} ...")
|
||||||
|
Path(logfile).unlink()
|
||||||
|
Logger.print_ok(f"{logfile} successfully removed!")
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
if mr_instances:
|
||||||
|
Logger.print_status("Removing KlipperScreen from update manager ...")
|
||||||
|
remove_config_section("update_manager KlipperScreen", mr_instances)
|
||||||
|
Logger.print_ok("KlipperScreen successfully removed from update manager!")
|
||||||
|
|
||||||
|
Logger.print_ok("KlipperScreen successfully removed!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error removing KlipperScreen:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def backup_klipperscreen_dir() -> None:
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(
|
||||||
|
"KlipperScreen",
|
||||||
|
source=KLIPPERSCREEN_DIR,
|
||||||
|
target=KLIPPERSCREEN_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
bm.backup_directory(
|
||||||
|
"KlipperScreen-env",
|
||||||
|
source=KLIPPERSCREEN_ENV,
|
||||||
|
target=KLIPPERSCREEN_BACKUP_DIR,
|
||||||
|
)
|
||||||
14
kiauh/components/log_uploads/__init__.py
Normal file
14
kiauh/components/log_uploads/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
from typing import Dict, Literal, Union
|
||||||
|
|
||||||
|
FileKey = Literal["filepath", "display_name"]
|
||||||
|
LogFile = Dict[FileKey, Union[str, Path]]
|
||||||
54
kiauh/components/log_uploads/log_upload_utils.py
Normal file
54
kiauh/components/log_uploads/log_upload_utils.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.log_uploads import LogFile
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_logfile_list() -> List[LogFile]:
|
||||||
|
cm = InstanceManager(Klipper)
|
||||||
|
log_dirs: List[Path] = [instance.log_dir for instance in cm.instances]
|
||||||
|
|
||||||
|
logfiles: List[LogFile] = []
|
||||||
|
for _dir in log_dirs:
|
||||||
|
for f in _dir.iterdir():
|
||||||
|
logfiles.append({"filepath": f, "display_name": get_display_name(f)})
|
||||||
|
|
||||||
|
return logfiles
|
||||||
|
|
||||||
|
|
||||||
|
def get_display_name(filepath: Path) -> str:
|
||||||
|
printer = " ".join(filepath.parts[-3].split("_")[:-1])
|
||||||
|
name = filepath.name
|
||||||
|
|
||||||
|
return f"{printer}: {name}"
|
||||||
|
|
||||||
|
|
||||||
|
def upload_logfile(logfile: LogFile) -> None:
|
||||||
|
file = logfile.get("filepath")
|
||||||
|
name = logfile.get("display_name")
|
||||||
|
Logger.print_status(f"Uploading the following logfile from {name} ...")
|
||||||
|
|
||||||
|
with open(file, "rb") as f:
|
||||||
|
headers = {"x-random": ""}
|
||||||
|
req = urllib.request.Request("http://paste.c-net.org/", headers=headers, data=f)
|
||||||
|
try:
|
||||||
|
response = urllib.request.urlopen(req)
|
||||||
|
link = response.read().decode("utf-8")
|
||||||
|
Logger.print_ok("Upload successful! Access it via the following link:")
|
||||||
|
Logger.print_ok(f">>>> {link}", False)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error("Uploading logfile failed!")
|
||||||
|
Logger.print_error(str(e))
|
||||||
61
kiauh/components/log_uploads/menus/log_upload_menu.py
Normal file
61
kiauh/components/log_uploads/menus/log_upload_menu.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.log_uploads.log_upload_utils import get_logfile_list, upload_logfile
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_YELLOW, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class LogUploadMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.logfile_list = get_logfile_list()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
f"{index}": Option(self.upload, False, opt_index=f"{index}")
|
||||||
|
for index in range(len(self.logfile_list))
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Log Upload ] "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| You can select the following logfiles for uploading: |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
for logfile in enumerate(self.logfile_list):
|
||||||
|
line = f"{logfile[0]}) {logfile[1].get('display_name')}"
|
||||||
|
menu += f"| {line:<54}|\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def upload(self, **kwargs):
|
||||||
|
index = int(kwargs.get("opt_index"))
|
||||||
|
upload_logfile(self.logfile_list[index])
|
||||||
16
kiauh/components/mobileraker/__init__.py
Normal file
16
kiauh/components/mobileraker/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git"
|
||||||
|
MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion")
|
||||||
|
MOBILERAKER_ENV = Path.home().joinpath("mobileraker-env")
|
||||||
|
MOBILERAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("mobileraker-backups")
|
||||||
211
kiauh/components/mobileraker/mobileraker.py
Normal file
211
kiauh/components/mobileraker/mobileraker.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import CalledProcessError, run
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.mobileraker import (
|
||||||
|
MOBILERAKER_BACKUP_DIR,
|
||||||
|
MOBILERAKER_DIR,
|
||||||
|
MOBILERAKER_ENV,
|
||||||
|
MOBILERAKER_REPO,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.common import check_install_dependencies, get_install_status
|
||||||
|
from utils.config_utils import add_config_section, remove_config_section
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.fs_utils import remove_with_sudo
|
||||||
|
from utils.git_utils import (
|
||||||
|
git_clone_wrapper,
|
||||||
|
git_pull_wrapper,
|
||||||
|
)
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import DialogType, Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
check_python_version,
|
||||||
|
cmd_sysctl_manage,
|
||||||
|
cmd_sysctl_service,
|
||||||
|
install_python_requirements,
|
||||||
|
)
|
||||||
|
from utils.types import ComponentStatus
|
||||||
|
|
||||||
|
|
||||||
|
def install_mobileraker() -> None:
|
||||||
|
Logger.print_status("Installing Mobileraker's companion ...")
|
||||||
|
|
||||||
|
if not check_python_version(3, 7):
|
||||||
|
return
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances = mr_im.instances
|
||||||
|
if not mr_instances:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.WARNING,
|
||||||
|
[
|
||||||
|
"Moonraker not found! Mobileraker's companion will not properly work "
|
||||||
|
"without a working Moonraker installation.",
|
||||||
|
"Mobileraker's companion's update manager configuration for Moonraker "
|
||||||
|
"will not be added to any moonraker.conf.",
|
||||||
|
],
|
||||||
|
end="",
|
||||||
|
)
|
||||||
|
if not get_confirm(
|
||||||
|
"Continue Mobileraker's companion installation?",
|
||||||
|
default_choice=False,
|
||||||
|
allow_go_back=True,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
package_list = ["git", "wget", "curl", "unzip", "dfu-util"]
|
||||||
|
check_install_dependencies(package_list)
|
||||||
|
|
||||||
|
git_clone_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR)
|
||||||
|
|
||||||
|
try:
|
||||||
|
script = f"{MOBILERAKER_DIR}/scripts/install.sh"
|
||||||
|
run(script, shell=True, check=True)
|
||||||
|
if mr_instances:
|
||||||
|
patch_mobileraker_update_manager(mr_instances)
|
||||||
|
mr_im.restart_all_instance()
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
"Moonraker is not installed! Cannot add Mobileraker's "
|
||||||
|
"companion to update manager!"
|
||||||
|
)
|
||||||
|
Logger.print_ok("Mobileraker's companion successfully installed!")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error installing Mobileraker's companion:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def patch_mobileraker_update_manager(instances: List[Moonraker]) -> None:
|
||||||
|
env_py = f"{MOBILERAKER_ENV}/bin/python"
|
||||||
|
add_config_section(
|
||||||
|
section="update_manager mobileraker",
|
||||||
|
instances=instances,
|
||||||
|
options=[
|
||||||
|
("type", "git_repo"),
|
||||||
|
("path", "mobileraker_companion"),
|
||||||
|
("orgin", MOBILERAKER_REPO),
|
||||||
|
("primary_branch", "main"),
|
||||||
|
("managed_services", "mobileraker"),
|
||||||
|
("env", env_py),
|
||||||
|
("requirements", "scripts/mobileraker-requirements.txt"),
|
||||||
|
("install_script", "scripts/install.sh"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_mobileraker() -> None:
|
||||||
|
try:
|
||||||
|
if not MOBILERAKER_DIR.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
"Mobileraker's companion does not seem to be installed! Skipping ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Updating Mobileraker's companion ...")
|
||||||
|
|
||||||
|
cmd_sysctl_service("mobileraker", "stop")
|
||||||
|
|
||||||
|
settings = KiauhSettings()
|
||||||
|
if settings.get("kiauh", "backup_before_update"):
|
||||||
|
backup_mobileraker_dir()
|
||||||
|
|
||||||
|
git_pull_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR)
|
||||||
|
|
||||||
|
requirements = MOBILERAKER_DIR.joinpath("/scripts/mobileraker-requirements.txt")
|
||||||
|
install_python_requirements(MOBILERAKER_ENV, requirements)
|
||||||
|
|
||||||
|
cmd_sysctl_service("mobileraker", "start")
|
||||||
|
|
||||||
|
Logger.print_ok("Mobileraker's companion updated successfully.", end="\n\n")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error updating Mobileraker's companion:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def get_mobileraker_status() -> ComponentStatus:
|
||||||
|
return get_install_status(
|
||||||
|
MOBILERAKER_DIR,
|
||||||
|
MOBILERAKER_ENV,
|
||||||
|
files=[SYSTEMD.joinpath("mobileraker.service")],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_mobileraker() -> None:
|
||||||
|
Logger.print_status("Removing Mobileraker's companion ...")
|
||||||
|
try:
|
||||||
|
if MOBILERAKER_DIR.exists():
|
||||||
|
Logger.print_status("Removing Mobileraker's companion directory ...")
|
||||||
|
shutil.rmtree(MOBILERAKER_DIR)
|
||||||
|
Logger.print_ok("Mobileraker's companion directory successfully removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_warn("Mobileraker's companion directory not found!")
|
||||||
|
|
||||||
|
if MOBILERAKER_ENV.exists():
|
||||||
|
Logger.print_status("Removing Mobileraker's companion environment ...")
|
||||||
|
shutil.rmtree(MOBILERAKER_ENV)
|
||||||
|
Logger.print_ok("Mobileraker's companion environment successfully removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_warn("Mobileraker's companion environment not found!")
|
||||||
|
|
||||||
|
service = SYSTEMD.joinpath("mobileraker.service")
|
||||||
|
if service.exists():
|
||||||
|
Logger.print_status("Removing mobileraker service ...")
|
||||||
|
cmd_sysctl_service(service, "stop")
|
||||||
|
cmd_sysctl_service(service, "disable")
|
||||||
|
remove_with_sudo(service)
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
cmd_sysctl_manage("reset-failed")
|
||||||
|
Logger.print_ok("Mobileraker's companion service successfully removed!")
|
||||||
|
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances: List[Klipper] = kl_im.instances
|
||||||
|
for instance in kl_instances:
|
||||||
|
logfile = instance.log_dir.joinpath("mobileraker.log")
|
||||||
|
if logfile.exists():
|
||||||
|
Logger.print_status(f"Removing {logfile} ...")
|
||||||
|
Path(logfile).unlink()
|
||||||
|
Logger.print_ok(f"{logfile} successfully removed!")
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
if mr_instances:
|
||||||
|
Logger.print_status(
|
||||||
|
"Removing Mobileraker's companion from update manager ..."
|
||||||
|
)
|
||||||
|
remove_config_section("update_manager mobileraker", mr_instances)
|
||||||
|
Logger.print_ok(
|
||||||
|
"Mobileraker's companion successfully removed from update manager!"
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.print_ok("Mobileraker's companion successfully removed!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error removing Mobileraker's companion:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def backup_mobileraker_dir() -> None:
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(
|
||||||
|
"mobileraker_companion",
|
||||||
|
source=MOBILERAKER_DIR,
|
||||||
|
target=MOBILERAKER_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
bm.backup_directory(
|
||||||
|
"mobileraker-env",
|
||||||
|
source=MOBILERAKER_ENV,
|
||||||
|
target=MOBILERAKER_BACKUP_DIR,
|
||||||
|
)
|
||||||
33
kiauh/components/moonraker/__init__.py
Normal file
33
kiauh/components/moonraker/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
MOONRAKER_DIR = Path.home().joinpath("moonraker")
|
||||||
|
MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env")
|
||||||
|
MOONRAKER_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-backups")
|
||||||
|
MOONRAKER_DB_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("moonraker-db-backups")
|
||||||
|
MOONRAKER_REQUIREMENTS_TXT = MOONRAKER_DIR.joinpath(
|
||||||
|
"scripts/moonraker-requirements.txt"
|
||||||
|
)
|
||||||
|
DEFAULT_MOONRAKER_PORT = 7125
|
||||||
|
|
||||||
|
# introduced due to
|
||||||
|
# https://github.com/Arksine/moonraker/issues/349
|
||||||
|
# https://github.com/Arksine/moonraker/pull/346
|
||||||
|
POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla")
|
||||||
|
POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules")
|
||||||
|
POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules")
|
||||||
|
POLKIT_SCRIPT = Path.home().joinpath("moonraker/scripts/set-policykit-rules.sh")
|
||||||
|
|
||||||
|
EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..."
|
||||||
29
kiauh/components/moonraker/assets/moonraker.conf
Normal file
29
kiauh/components/moonraker/assets/moonraker.conf
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[server]
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: %PORT%
|
||||||
|
klippy_uds_address: %UDS%
|
||||||
|
|
||||||
|
[authorization]
|
||||||
|
trusted_clients:
|
||||||
|
10.0.0.0/8
|
||||||
|
127.0.0.0/8
|
||||||
|
169.254.0.0/16
|
||||||
|
172.16.0.0/12
|
||||||
|
192.168.0.0/16
|
||||||
|
FE80::/10
|
||||||
|
::1/128
|
||||||
|
cors_domains:
|
||||||
|
*.lan
|
||||||
|
*.local
|
||||||
|
*://localhost
|
||||||
|
*://localhost:*
|
||||||
|
*://my.mainsail.xyz
|
||||||
|
*://app.fluidd.xyz
|
||||||
|
|
||||||
|
[octoprint_compat]
|
||||||
|
|
||||||
|
[history]
|
||||||
|
|
||||||
|
[update_manager]
|
||||||
|
channel: dev
|
||||||
|
refresh_interval: 168
|
||||||
1
kiauh/components/moonraker/assets/moonraker.env
Normal file
1
kiauh/components/moonraker/assets/moonraker.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%"
|
||||||
19
kiauh/components/moonraker/assets/moonraker.service
Normal file
19
kiauh/components/moonraker/assets/moonraker.service
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=API Server for Klipper SV1
|
||||||
|
Documentation=https://moonraker.readthedocs.io/
|
||||||
|
Requires=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=%USER%
|
||||||
|
SupplementaryGroups=moonraker-admin
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=%MOONRAKER_DIR%
|
||||||
|
EnvironmentFile=%ENV_FILE%
|
||||||
|
ExecStart=%ENV%/bin/python $MOONRAKER_ARGS
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
0
kiauh/components/moonraker/menus/__init__.py
Normal file
0
kiauh/components/moonraker/menus/__init__.py
Normal file
126
kiauh/components/moonraker/menus/moonraker_remove_menu.py
Normal file
126
kiauh/components/moonraker/menus/moonraker_remove_menu.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.moonraker import moonraker_remove
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
class MoonrakerRemoveMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.remove_moonraker_service = False
|
||||||
|
self.remove_moonraker_dir = False
|
||||||
|
self.remove_moonraker_env = False
|
||||||
|
self.remove_moonraker_polkit = False
|
||||||
|
self.delete_moonraker_logs = False
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else RemoveMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(method=self.toggle_all, menu=False),
|
||||||
|
"1": Option(method=self.toggle_remove_moonraker_service, menu=False),
|
||||||
|
"2": Option(method=self.toggle_remove_moonraker_dir, menu=False),
|
||||||
|
"3": Option(method=self.toggle_remove_moonraker_env, menu=False),
|
||||||
|
"4": Option(method=self.toggle_remove_moonraker_polkit, menu=False),
|
||||||
|
"5": Option(method=self.toggle_delete_moonraker_logs, menu=False),
|
||||||
|
"c": Option(method=self.run_removal_process, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = " [ Remove Moonraker ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
|
||||||
|
unchecked = "[ ]"
|
||||||
|
o1 = checked if self.remove_moonraker_service else unchecked
|
||||||
|
o2 = checked if self.remove_moonraker_dir else unchecked
|
||||||
|
o3 = checked if self.remove_moonraker_env else unchecked
|
||||||
|
o4 = checked if self.remove_moonraker_polkit else unchecked
|
||||||
|
o5 = checked if self.delete_moonraker_logs else unchecked
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Enter a number and hit enter to select / deselect |
|
||||||
|
| the specific option for removal. |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Select everything |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) {o1} Remove Service |
|
||||||
|
| 2) {o2} Remove Local Repository |
|
||||||
|
| 3) {o3} Remove Python Environment |
|
||||||
|
| 4) {o4} Remove Policy Kit Rules |
|
||||||
|
| 5) {o5} Delete all Log-Files |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| C) Continue |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def toggle_all(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_service = True
|
||||||
|
self.remove_moonraker_dir = True
|
||||||
|
self.remove_moonraker_env = True
|
||||||
|
self.remove_moonraker_polkit = True
|
||||||
|
self.delete_moonraker_logs = True
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_service(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_service = not self.remove_moonraker_service
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_dir(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_dir = not self.remove_moonraker_dir
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_env(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_env = not self.remove_moonraker_env
|
||||||
|
|
||||||
|
def toggle_remove_moonraker_polkit(self, **kwargs) -> None:
|
||||||
|
self.remove_moonraker_polkit = not self.remove_moonraker_polkit
|
||||||
|
|
||||||
|
def toggle_delete_moonraker_logs(self, **kwargs) -> None:
|
||||||
|
self.delete_moonraker_logs = not self.delete_moonraker_logs
|
||||||
|
|
||||||
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
|
if (
|
||||||
|
not self.remove_moonraker_service
|
||||||
|
and not self.remove_moonraker_dir
|
||||||
|
and not self.remove_moonraker_env
|
||||||
|
and not self.remove_moonraker_polkit
|
||||||
|
and not self.delete_moonraker_logs
|
||||||
|
):
|
||||||
|
error = f"{COLOR_RED}Nothing selected! Select options to remove first.{RESET_FORMAT}"
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
moonraker_remove.run_moonraker_removal(
|
||||||
|
self.remove_moonraker_service,
|
||||||
|
self.remove_moonraker_dir,
|
||||||
|
self.remove_moonraker_env,
|
||||||
|
self.remove_moonraker_polkit,
|
||||||
|
self.delete_moonraker_logs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.remove_moonraker_service = False
|
||||||
|
self.remove_moonraker_dir = False
|
||||||
|
self.remove_moonraker_env = False
|
||||||
|
self.remove_moonraker_polkit = False
|
||||||
|
self.delete_moonraker_logs = False
|
||||||
154
kiauh/components/moonraker/moonraker.py
Normal file
154
kiauh/components/moonraker/moonraker.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from components.moonraker import MODULE_PATH, MOONRAKER_DIR, MOONRAKER_ENV_DIR
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class Moonraker(BaseInstance):
|
||||||
|
@classmethod
|
||||||
|
def blacklist(cls) -> List[str]:
|
||||||
|
return ["None", "mcu"]
|
||||||
|
|
||||||
|
def __init__(self, suffix: str = ""):
|
||||||
|
super().__init__(instance_type=self, suffix=suffix)
|
||||||
|
self.moonraker_dir: Path = MOONRAKER_DIR
|
||||||
|
self.env_dir: Path = MOONRAKER_ENV_DIR
|
||||||
|
self.cfg_file = self.cfg_dir.joinpath("moonraker.conf")
|
||||||
|
self.port = self._get_port()
|
||||||
|
self.backup_dir = self.data_dir.joinpath("backup")
|
||||||
|
self.certs_dir = self.data_dir.joinpath("certs")
|
||||||
|
self._db_dir = self.data_dir.joinpath("database")
|
||||||
|
self._comms_dir = self.data_dir.joinpath("comms")
|
||||||
|
self.log = self.log_dir.joinpath("moonraker.log")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_dir(self) -> Path:
|
||||||
|
return self._db_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comms_dir(self) -> Path:
|
||||||
|
return self._comms_dir
|
||||||
|
|
||||||
|
def create(self, create_example_cfg: bool = False) -> None:
|
||||||
|
Logger.print_status("Creating new Moonraker Instance ...")
|
||||||
|
service_template_path = MODULE_PATH.joinpath("assets/moonraker.service")
|
||||||
|
env_template_file_path = MODULE_PATH.joinpath("assets/moonraker.env")
|
||||||
|
service_file_name = self.get_service_file_name(extension=True)
|
||||||
|
service_file_target = SYSTEMD.joinpath(service_file_name)
|
||||||
|
env_file_target = self.sysd_dir.joinpath("moonraker.env")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.create_folders([self.backup_dir, self.certs_dir, self._db_dir])
|
||||||
|
self.write_service_file(
|
||||||
|
service_template_path, service_file_target, env_file_target
|
||||||
|
)
|
||||||
|
self.write_env_file(env_template_file_path, env_file_target)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Error creating service file {service_file_target}: {e}"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Error writing file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
service_file = self.get_service_file_name(extension=True)
|
||||||
|
service_file_path = self.get_service_file_path()
|
||||||
|
|
||||||
|
Logger.print_status(f"Deleting Moonraker Instance: {service_file}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = ["sudo", "rm", "-f", service_file_path]
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
Logger.print_ok(f"Service file deleted: {service_file_path}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error deleting service file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def write_service_file(
|
||||||
|
self,
|
||||||
|
service_template_path: Path,
|
||||||
|
service_file_target: Path,
|
||||||
|
env_file_target: Path,
|
||||||
|
) -> None:
|
||||||
|
service_content = self._prep_service_file(
|
||||||
|
service_template_path, env_file_target
|
||||||
|
)
|
||||||
|
command = ["sudo", "tee", service_file_target]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
input=service_content.encode(),
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
Logger.print_ok(f"Service file created: {service_file_target}")
|
||||||
|
|
||||||
|
def write_env_file(
|
||||||
|
self, env_template_file_path: Path, env_file_target: Path
|
||||||
|
) -> None:
|
||||||
|
env_file_content = self._prep_env_file(env_template_file_path)
|
||||||
|
with open(env_file_target, "w") as env_file:
|
||||||
|
env_file.write(env_file_content)
|
||||||
|
Logger.print_ok(f"Env file created: {env_file_target}")
|
||||||
|
|
||||||
|
def _prep_service_file(
|
||||||
|
self, service_template_path: Path, env_file_path: Path
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
with open(service_template_path, "r") as template_file:
|
||||||
|
template_content = template_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {service_template_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
service_content = template_content.replace("%USER%", self.user)
|
||||||
|
service_content = service_content.replace(
|
||||||
|
"%MOONRAKER_DIR%", str(self.moonraker_dir)
|
||||||
|
)
|
||||||
|
service_content = service_content.replace("%ENV%", str(self.env_dir))
|
||||||
|
service_content = service_content.replace("%ENV_FILE%", str(env_file_path))
|
||||||
|
return service_content
|
||||||
|
|
||||||
|
def _prep_env_file(self, env_template_file_path: Path) -> str:
|
||||||
|
try:
|
||||||
|
with open(env_template_file_path, "r") as env_file:
|
||||||
|
env_template_file_content = env_file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Unable to open {env_template_file_path} - File not found"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
env_file_content = env_template_file_content.replace(
|
||||||
|
"%MOONRAKER_DIR%", str(self.moonraker_dir)
|
||||||
|
)
|
||||||
|
env_file_content = env_file_content.replace(
|
||||||
|
"%PRINTER_DATA%", str(self.data_dir)
|
||||||
|
)
|
||||||
|
return env_file_content
|
||||||
|
|
||||||
|
def _get_port(self) -> Union[int, None]:
|
||||||
|
if not self.cfg_file.is_file():
|
||||||
|
return None
|
||||||
|
|
||||||
|
cm = ConfigManager(cfg_file=self.cfg_file)
|
||||||
|
port = cm.get_value("server", "port")
|
||||||
|
|
||||||
|
return int(port) if port is not None else port
|
||||||
70
kiauh/components/moonraker/moonraker_dialogs.py
Normal file
70
kiauh/components/moonraker/moonraker_dialogs.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.menus.base_menu import print_back_footer
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
def print_moonraker_overview(
|
||||||
|
klipper_instances: List[Klipper],
|
||||||
|
moonraker_instances: List[Moonraker],
|
||||||
|
show_index=False,
|
||||||
|
show_select_all=False,
|
||||||
|
):
|
||||||
|
headline = f"{COLOR_GREEN}The following instances were found:{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
|{headline:^64}|
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if show_select_all:
|
||||||
|
select_all = f"{COLOR_YELLOW}a) Select all{RESET_FORMAT}"
|
||||||
|
dialog += f"| {select_all:<63}|\n"
|
||||||
|
dialog += "| |\n"
|
||||||
|
|
||||||
|
instance_map = {
|
||||||
|
k.get_service_file_name(): (
|
||||||
|
k.get_service_file_name().replace("klipper", "moonraker")
|
||||||
|
if k.suffix in [m.suffix for m in moonraker_instances]
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
for k in klipper_instances
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, k in enumerate(instance_map):
|
||||||
|
mr_name = instance_map.get(k)
|
||||||
|
m = f"<-> {mr_name}" if mr_name != "" else ""
|
||||||
|
line = f"{COLOR_CYAN}{f'{i})' if show_index else '●'} {k} {m} {RESET_FORMAT}"
|
||||||
|
dialog += f"| {line:<63}|\n"
|
||||||
|
|
||||||
|
warn_l1 = f"{COLOR_YELLOW}PLEASE NOTE: {RESET_FORMAT}"
|
||||||
|
warn_l2 = f"{COLOR_YELLOW}If you select an instance with an existing Moonraker{RESET_FORMAT}"
|
||||||
|
warn_l3 = f"{COLOR_YELLOW}instance, that Moonraker instance will be re-created!{RESET_FORMAT}"
|
||||||
|
warning = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
| |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {warn_l1:<63}|
|
||||||
|
| {warn_l2:<63}|
|
||||||
|
| {warn_l3:<63}|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
dialog += warning
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
160
kiauh/components/moonraker/moonraker_remove.py
Normal file
160
kiauh/components/moonraker/moonraker_remove.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
import subprocess
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from components.klipper.klipper_dialogs import print_instance_overview
|
||||||
|
from components.moonraker import MOONRAKER_DIR, MOONRAKER_ENV_DIR
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.fs_utils import remove_file
|
||||||
|
from utils.input_utils import get_selection_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import cmd_sysctl_manage
|
||||||
|
|
||||||
|
|
||||||
|
def run_moonraker_removal(
|
||||||
|
remove_service: bool,
|
||||||
|
remove_dir: bool,
|
||||||
|
remove_env: bool,
|
||||||
|
remove_polkit: bool,
|
||||||
|
delete_logs: bool,
|
||||||
|
) -> None:
|
||||||
|
im = InstanceManager(Moonraker)
|
||||||
|
|
||||||
|
if remove_service:
|
||||||
|
Logger.print_status("Removing Moonraker instances ...")
|
||||||
|
if im.instances:
|
||||||
|
instances_to_remove = select_instances_to_remove(im.instances)
|
||||||
|
remove_instances(im, instances_to_remove)
|
||||||
|
else:
|
||||||
|
Logger.print_info("No Moonraker Services installed! Skipped ...")
|
||||||
|
|
||||||
|
if (remove_polkit or remove_dir or remove_env) and im.instances:
|
||||||
|
Logger.print_warn("There are still other Moonraker services installed!")
|
||||||
|
Logger.print_warn("Therefor the following parts cannot be removed:")
|
||||||
|
Logger.print_warn(
|
||||||
|
"""
|
||||||
|
● Moonraker PolicyKit rules
|
||||||
|
● Moonraker local repository
|
||||||
|
● Moonraker Python environment
|
||||||
|
""",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if remove_polkit:
|
||||||
|
Logger.print_status("Removing all Moonraker policykit rules ...")
|
||||||
|
remove_polkit_rules()
|
||||||
|
if remove_dir:
|
||||||
|
Logger.print_status("Removing Moonraker local repository ...")
|
||||||
|
remove_moonraker_dir()
|
||||||
|
if remove_env:
|
||||||
|
Logger.print_status("Removing Moonraker Python environment ...")
|
||||||
|
remove_moonraker_env()
|
||||||
|
|
||||||
|
# delete moonraker logs of all instances
|
||||||
|
if delete_logs:
|
||||||
|
Logger.print_status("Removing all Moonraker logs ...")
|
||||||
|
delete_moonraker_logs(im.instances)
|
||||||
|
|
||||||
|
|
||||||
|
def select_instances_to_remove(
|
||||||
|
instances: List[Moonraker],
|
||||||
|
) -> Union[List[Moonraker], None]:
|
||||||
|
print_instance_overview(instances, show_index=True, show_select_all=True)
|
||||||
|
|
||||||
|
options = [str(i) for i in range(len(instances))]
|
||||||
|
options.extend(["a", "A", "b", "B"])
|
||||||
|
|
||||||
|
selection = get_selection_input("Select Moonraker instance to remove", options)
|
||||||
|
|
||||||
|
instances_to_remove = []
|
||||||
|
if selection == "b".lower():
|
||||||
|
return None
|
||||||
|
elif selection == "a".lower():
|
||||||
|
instances_to_remove.extend(instances)
|
||||||
|
else:
|
||||||
|
instance = instances[int(selection)]
|
||||||
|
instances_to_remove.append(instance)
|
||||||
|
|
||||||
|
return instances_to_remove
|
||||||
|
|
||||||
|
|
||||||
|
def remove_instances(
|
||||||
|
instance_manager: InstanceManager,
|
||||||
|
instance_list: List[Moonraker],
|
||||||
|
) -> None:
|
||||||
|
for instance in instance_list:
|
||||||
|
Logger.print_status(f"Removing instance {instance.get_service_file_name()} ...")
|
||||||
|
instance_manager.current_instance = instance
|
||||||
|
instance_manager.stop_instance()
|
||||||
|
instance_manager.disable_instance()
|
||||||
|
instance_manager.delete_instance()
|
||||||
|
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_moonraker_dir() -> None:
|
||||||
|
if not MOONRAKER_DIR.exists():
|
||||||
|
Logger.print_info(f"'{MOONRAKER_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(MOONRAKER_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{MOONRAKER_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_moonraker_env() -> None:
|
||||||
|
if not MOONRAKER_ENV_DIR.exists():
|
||||||
|
Logger.print_info(f"'{MOONRAKER_ENV_DIR}' does not exist. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(MOONRAKER_ENV_DIR)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{MOONRAKER_ENV_DIR}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_polkit_rules() -> None:
|
||||||
|
if not MOONRAKER_DIR.exists():
|
||||||
|
log = "Cannot remove policykit rules. Moonraker directory not found."
|
||||||
|
Logger.print_warn(log)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = [
|
||||||
|
f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh",
|
||||||
|
"--clear",
|
||||||
|
]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error while removing policykit rules: {e}")
|
||||||
|
|
||||||
|
Logger.print_ok("Policykit rules successfully removed!")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_moonraker_logs(instances: List[Moonraker]) -> None:
|
||||||
|
all_logfiles = []
|
||||||
|
for instance in instances:
|
||||||
|
all_logfiles = list(instance.log_dir.glob("moonraker.log*"))
|
||||||
|
if not all_logfiles:
|
||||||
|
Logger.print_info("No Moonraker logs found. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
for log in all_logfiles:
|
||||||
|
Logger.print_status(f"Remove '{log}'")
|
||||||
|
remove_file(log)
|
||||||
218
kiauh/components/moonraker/moonraker_setup.py
Normal file
218
kiauh/components/moonraker/moonraker_setup.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker import (
|
||||||
|
EXIT_MOONRAKER_SETUP,
|
||||||
|
MOONRAKER_DIR,
|
||||||
|
MOONRAKER_ENV_DIR,
|
||||||
|
MOONRAKER_REQUIREMENTS_TXT,
|
||||||
|
POLKIT_FILE,
|
||||||
|
POLKIT_LEGACY_FILE,
|
||||||
|
POLKIT_SCRIPT,
|
||||||
|
POLKIT_USR_FILE,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.moonraker.moonraker_dialogs import print_moonraker_overview
|
||||||
|
from components.moonraker.moonraker_utils import (
|
||||||
|
backup_moonraker_dir,
|
||||||
|
create_example_moonraker_conf,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
enable_mainsail_remotemode,
|
||||||
|
get_existing_clients,
|
||||||
|
)
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.common import check_install_dependencies
|
||||||
|
from utils.fs_utils import check_file_exist
|
||||||
|
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
|
||||||
|
from utils.input_utils import (
|
||||||
|
get_confirm,
|
||||||
|
get_selection_input,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
check_python_version,
|
||||||
|
cmd_sysctl_manage,
|
||||||
|
create_python_venv,
|
||||||
|
install_python_requirements,
|
||||||
|
parse_packages_from_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_moonraker() -> None:
|
||||||
|
if not check_moonraker_install_requirements():
|
||||||
|
return
|
||||||
|
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
klipper_instances = kl_im.instances
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
moonraker_instances = mr_im.instances
|
||||||
|
|
||||||
|
selected_klipper_instance = 0
|
||||||
|
if len(klipper_instances) > 1:
|
||||||
|
print_moonraker_overview(
|
||||||
|
klipper_instances,
|
||||||
|
moonraker_instances,
|
||||||
|
show_index=True,
|
||||||
|
show_select_all=True,
|
||||||
|
)
|
||||||
|
options = [str(i) for i in range(len(klipper_instances))]
|
||||||
|
options.extend(["a", "A", "b", "B"])
|
||||||
|
question = "Select Klipper instance to setup Moonraker for"
|
||||||
|
selected_klipper_instance = get_selection_input(question, options).lower()
|
||||||
|
|
||||||
|
instance_names = []
|
||||||
|
if selected_klipper_instance == "b":
|
||||||
|
Logger.print_status(EXIT_MOONRAKER_SETUP)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif selected_klipper_instance == "a":
|
||||||
|
for instance in klipper_instances:
|
||||||
|
instance_names.append(instance.suffix)
|
||||||
|
|
||||||
|
else:
|
||||||
|
index = int(selected_klipper_instance)
|
||||||
|
instance_names.append(klipper_instances[index].suffix)
|
||||||
|
|
||||||
|
create_example_cfg = get_confirm("Create example moonraker.conf?")
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_install_dependencies(["git"])
|
||||||
|
setup_moonraker_prerequesites()
|
||||||
|
install_moonraker_polkit()
|
||||||
|
|
||||||
|
used_ports_map = {
|
||||||
|
instance.suffix: instance.port for instance in moonraker_instances
|
||||||
|
}
|
||||||
|
for name in instance_names:
|
||||||
|
current_instance = Moonraker(suffix=name)
|
||||||
|
|
||||||
|
mr_im.current_instance = current_instance
|
||||||
|
mr_im.create_instance()
|
||||||
|
mr_im.enable_instance()
|
||||||
|
|
||||||
|
if create_example_cfg:
|
||||||
|
# if a webclient and/or it's config is installed, patch
|
||||||
|
# its update section to the config
|
||||||
|
clients = get_existing_clients()
|
||||||
|
create_example_moonraker_conf(current_instance, used_ports_map, clients)
|
||||||
|
|
||||||
|
mr_im.start_instance()
|
||||||
|
|
||||||
|
cmd_sysctl_manage("daemon-reload")
|
||||||
|
|
||||||
|
# if mainsail is installed, and we installed
|
||||||
|
# multiple moonraker instances, we enable mainsails remote mode
|
||||||
|
if MainsailData().client_dir.exists() and len(mr_im.instances) > 1:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error while installing Moonraker: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def check_moonraker_install_requirements() -> bool:
|
||||||
|
def check_klipper_instances() -> bool:
|
||||||
|
if len(InstanceManager(Klipper).instances) >= 1:
|
||||||
|
return True
|
||||||
|
|
||||||
|
Logger.print_warn("Klipper not installed!")
|
||||||
|
Logger.print_warn("Moonraker cannot be installed! Install Klipper first.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return check_python_version(3, 7) and check_klipper_instances()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_moonraker_prerequesites() -> None:
|
||||||
|
settings = KiauhSettings()
|
||||||
|
repo = settings.get("moonraker", "repo_url")
|
||||||
|
branch = settings.get("moonraker", "branch")
|
||||||
|
|
||||||
|
git_clone_wrapper(repo, MOONRAKER_DIR, branch)
|
||||||
|
|
||||||
|
# install moonraker dependencies and create python virtualenv
|
||||||
|
install_moonraker_packages(MOONRAKER_DIR)
|
||||||
|
create_python_venv(MOONRAKER_ENV_DIR)
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT)
|
||||||
|
|
||||||
|
|
||||||
|
def install_moonraker_packages(moonraker_dir: Path) -> None:
|
||||||
|
install_script = moonraker_dir.joinpath("scripts/install-moonraker.sh")
|
||||||
|
deps_json = MOONRAKER_DIR.joinpath("scripts/system-dependencies.json")
|
||||||
|
moonraker_deps = []
|
||||||
|
|
||||||
|
if deps_json.exists():
|
||||||
|
with open(deps_json, "r") as deps:
|
||||||
|
moonraker_deps = json.load(deps).get("debian", [])
|
||||||
|
elif install_script.exists():
|
||||||
|
moonraker_deps = parse_packages_from_file(install_script)
|
||||||
|
|
||||||
|
if not moonraker_deps:
|
||||||
|
raise ValueError("Error reading Moonraker dependencies!")
|
||||||
|
|
||||||
|
check_install_dependencies(moonraker_deps)
|
||||||
|
|
||||||
|
|
||||||
|
def install_moonraker_polkit() -> None:
|
||||||
|
Logger.print_status("Installing Moonraker policykit rules ...")
|
||||||
|
|
||||||
|
legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True)
|
||||||
|
polkit_file_exists = check_file_exist(POLKIT_FILE, True)
|
||||||
|
usr_file_exists = check_file_exist(POLKIT_USR_FILE, True)
|
||||||
|
|
||||||
|
if legacy_file_exists or (polkit_file_exists and usr_file_exists):
|
||||||
|
Logger.print_info("Moonraker policykit rules are already installed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = [POLKIT_SCRIPT, "--disable-systemctl"]
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0 or result.stderr:
|
||||||
|
Logger.print_error(f"{result.stderr}", False)
|
||||||
|
Logger.print_error("Installing Moonraker policykit rules failed!")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_ok("Moonraker policykit rules successfully installed!")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log = f"Error while installing Moonraker policykit rules: {e.stderr.decode()}"
|
||||||
|
Logger.print_error(log)
|
||||||
|
|
||||||
|
|
||||||
|
def update_moonraker() -> None:
|
||||||
|
if not get_confirm("Update Moonraker now?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = KiauhSettings()
|
||||||
|
if settings.get("kiauh", "backup_before_update"):
|
||||||
|
backup_moonraker_dir()
|
||||||
|
|
||||||
|
instance_manager = InstanceManager(Moonraker)
|
||||||
|
instance_manager.stop_all_instance()
|
||||||
|
|
||||||
|
git_pull_wrapper(
|
||||||
|
repo=settings.get("moonraker", "repo_url"), target_dir=MOONRAKER_DIR
|
||||||
|
)
|
||||||
|
|
||||||
|
# install possible new system packages
|
||||||
|
install_moonraker_packages(MOONRAKER_DIR)
|
||||||
|
# install possible new python dependencies
|
||||||
|
install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQUIREMENTS_TXT)
|
||||||
|
|
||||||
|
instance_manager.start_all_instance()
|
||||||
189
kiauh/components/moonraker/moonraker_utils.py
Normal file
189
kiauh/components/moonraker/moonraker_utils.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from components.moonraker import (
|
||||||
|
DEFAULT_MOONRAKER_PORT,
|
||||||
|
MODULE_PATH,
|
||||||
|
MOONRAKER_BACKUP_DIR,
|
||||||
|
MOONRAKER_DB_BACKUP_DIR,
|
||||||
|
MOONRAKER_DIR,
|
||||||
|
MOONRAKER_ENV_DIR,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import BaseWebClient
|
||||||
|
from components.webui_client.client_utils import enable_mainsail_remotemode
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.common import get_install_status
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
get_ipv4_addr,
|
||||||
|
)
|
||||||
|
from utils.types import ComponentStatus
|
||||||
|
|
||||||
|
|
||||||
|
def get_moonraker_status() -> ComponentStatus:
|
||||||
|
return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker)
|
||||||
|
|
||||||
|
|
||||||
|
def create_example_moonraker_conf(
|
||||||
|
instance: Moonraker,
|
||||||
|
ports_map: Dict[str, int],
|
||||||
|
clients: Optional[List[BaseWebClient]] = None,
|
||||||
|
) -> None:
|
||||||
|
Logger.print_status(f"Creating example moonraker.conf in '{instance.cfg_dir}'")
|
||||||
|
if instance.cfg_file.is_file():
|
||||||
|
Logger.print_info(f"'{instance.cfg_file}' already exists.")
|
||||||
|
return
|
||||||
|
|
||||||
|
source = MODULE_PATH.joinpath("assets/moonraker.conf")
|
||||||
|
target = instance.cfg_file
|
||||||
|
try:
|
||||||
|
shutil.copy(source, target)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to create example moonraker.conf:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
ports = [
|
||||||
|
ports_map.get(instance)
|
||||||
|
for instance in ports_map
|
||||||
|
if ports_map.get(instance) is not None
|
||||||
|
]
|
||||||
|
if ports_map.get(instance.suffix) is None:
|
||||||
|
# this could be improved to not increment the max value of the ports list and assign it as the port
|
||||||
|
# as it can lead to situation where the port for e.g. instance moonraker-2 becomes 7128 if the port
|
||||||
|
# of moonraker-1 is 7125 and moonraker-3 is 7127 and there are moonraker.conf files for moonraker-1
|
||||||
|
# and moonraker-3 already. though, there does not seem to be a very reliable way of always assigning
|
||||||
|
# the correct port to each instance and the user will likely be required to correct the value manually.
|
||||||
|
port = max(ports) + 1 if ports else DEFAULT_MOONRAKER_PORT
|
||||||
|
else:
|
||||||
|
port = ports_map.get(instance.suffix)
|
||||||
|
|
||||||
|
ports_map[instance.suffix] = port
|
||||||
|
|
||||||
|
ip = get_ipv4_addr().split(".")[:2]
|
||||||
|
ip.extend(["0", "0/16"])
|
||||||
|
uds = instance.comms_dir.joinpath("klippy.sock")
|
||||||
|
|
||||||
|
cm = ConfigManager(target)
|
||||||
|
trusted_clients = f"\n{'.'.join(ip)}"
|
||||||
|
trusted_clients += cm.get_value("authorization", "trusted_clients")
|
||||||
|
|
||||||
|
cm.set_value("server", "port", str(port))
|
||||||
|
cm.set_value("server", "klippy_uds_address", str(uds))
|
||||||
|
cm.set_value("authorization", "trusted_clients", trusted_clients)
|
||||||
|
|
||||||
|
# add existing client and client configs in the update section
|
||||||
|
if clients is not None and len(clients) > 0:
|
||||||
|
for c in clients:
|
||||||
|
# client part
|
||||||
|
c_section = f"update_manager {c.name}"
|
||||||
|
c_options = [
|
||||||
|
("type", "web"),
|
||||||
|
("channel", "stable"),
|
||||||
|
("repo", c.repo_path),
|
||||||
|
("path", c.client_dir),
|
||||||
|
]
|
||||||
|
cm.config.add_section(section=c_section)
|
||||||
|
for option in c_options:
|
||||||
|
cm.config.set(c_section, option[0], option[1])
|
||||||
|
|
||||||
|
# client config part
|
||||||
|
c_config = c.client_config
|
||||||
|
if c_config.config_dir.exists():
|
||||||
|
c_config_section = f"update_manager {c_config.name}"
|
||||||
|
c_config_options = [
|
||||||
|
("type", "git_repo"),
|
||||||
|
("primary_branch", "master"),
|
||||||
|
("path", c_config.config_dir),
|
||||||
|
("origin", c_config.repo_url),
|
||||||
|
("managed_services", "klipper"),
|
||||||
|
]
|
||||||
|
cm.config.add_section(section=c_config_section)
|
||||||
|
for option in c_config_options:
|
||||||
|
cm.config.set(c_config_section, option[0], option[1])
|
||||||
|
|
||||||
|
cm.write_config()
|
||||||
|
Logger.print_ok(f"Example moonraker.conf created in '{instance.cfg_dir}'")
|
||||||
|
|
||||||
|
|
||||||
|
def moonraker_to_multi_conversion(new_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Converts the first instance in the List of Moonraker instances to an instance
|
||||||
|
with a new name. This method will be called when converting from a single Klipper
|
||||||
|
instance install to a multi instance install when Moonraker is also already
|
||||||
|
installed with a single instance.
|
||||||
|
:param new_name: new name the previous single instance is renamed to
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
im = InstanceManager(Moonraker)
|
||||||
|
instances: List[Moonraker] = im.instances
|
||||||
|
if not instances:
|
||||||
|
return
|
||||||
|
|
||||||
|
# in case there are multiple Moonraker instances, we don't want to do anything
|
||||||
|
if len(instances) > 1:
|
||||||
|
Logger.print_info("More than a single Moonraker instance found. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Convert Moonraker single to multi instance ...")
|
||||||
|
|
||||||
|
# remove the old single instance
|
||||||
|
im.current_instance = im.instances[0]
|
||||||
|
im.stop_instance()
|
||||||
|
im.disable_instance()
|
||||||
|
im.delete_instance()
|
||||||
|
|
||||||
|
# create a new moonraker instance with the new name
|
||||||
|
new_instance = Moonraker(suffix=new_name)
|
||||||
|
im.current_instance = new_instance
|
||||||
|
|
||||||
|
# patch the server sections klippy_uds_address value to match the new printer_data foldername
|
||||||
|
cm = ConfigManager(new_instance.cfg_file)
|
||||||
|
if cm.config.has_section("server"):
|
||||||
|
cm.set_value(
|
||||||
|
"server",
|
||||||
|
"klippy_uds_address",
|
||||||
|
str(new_instance.comms_dir.joinpath("klippy.sock")),
|
||||||
|
)
|
||||||
|
cm.write_config()
|
||||||
|
|
||||||
|
# create, enable and start the new moonraker instance
|
||||||
|
im.create_instance()
|
||||||
|
im.enable_instance()
|
||||||
|
im.start_instance()
|
||||||
|
|
||||||
|
# if mainsail is installed, we enable mainsails remote mode
|
||||||
|
if MainsailData().client_dir.exists() and len(im.instances) > 1:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
|
||||||
|
|
||||||
|
def backup_moonraker_dir():
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory("moonraker", source=MOONRAKER_DIR, target=MOONRAKER_BACKUP_DIR)
|
||||||
|
bm.backup_directory(
|
||||||
|
"moonraker-env", source=MOONRAKER_ENV_DIR, target=MOONRAKER_BACKUP_DIR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_moonraker_db_dir() -> None:
|
||||||
|
im = InstanceManager(Moonraker)
|
||||||
|
instances: List[Moonraker] = im.instances
|
||||||
|
bm = BackupManager()
|
||||||
|
|
||||||
|
for instance in instances:
|
||||||
|
name = f"database-{instance.data_dir_name}"
|
||||||
|
bm.backup_directory(
|
||||||
|
name, source=instance.db_dir, target=MOONRAKER_DB_BACKUP_DIR
|
||||||
|
)
|
||||||
12
kiauh/components/webui_client/__init__.py
Normal file
12
kiauh/components/webui_client/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
95
kiauh/components/webui_client/assets/nginx_cfg
Normal file
95
kiauh/components/webui_client/assets/nginx_cfg
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
server {
|
||||||
|
listen %PORT%;
|
||||||
|
# uncomment the next line to activate IPv6
|
||||||
|
# listen [::]:%PORT%;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/%NAME%-access.log;
|
||||||
|
error_log /var/log/nginx/%NAME%-error.log;
|
||||||
|
|
||||||
|
# disable this section on smaller hardware like a pi zero
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_comp_level 4;
|
||||||
|
gzip_buffers 16 8k;
|
||||||
|
gzip_http_version 1.1;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml;
|
||||||
|
|
||||||
|
# web_path from %NAME% static files
|
||||||
|
root %ROOT_DIR%;
|
||||||
|
|
||||||
|
index index.html;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# disable max upload size checks
|
||||||
|
client_max_body_size 0;
|
||||||
|
|
||||||
|
# disable proxy request buffering
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /websocket {
|
||||||
|
proxy_pass http://apiserver/websocket;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(printer|api|access|machine|server)/ {
|
||||||
|
proxy_pass http://apiserver$request_uri;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Scheme $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer1/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam2/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer2/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam3/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer3/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /webcam4/ {
|
||||||
|
postpone_output 0;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_ignore_headers X-Accel-Buffering;
|
||||||
|
access_log off;
|
||||||
|
error_log off;
|
||||||
|
proxy_pass http://mjpgstreamer4/;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
kiauh/components/webui_client/base_data.py
Normal file
112
kiauh/components/webui_client/base_data.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 abc import ABC, abstractmethod
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class WebClientType(Enum):
|
||||||
|
MAINSAIL: str = "mainsail"
|
||||||
|
FLUIDD: str = "fluidd"
|
||||||
|
|
||||||
|
|
||||||
|
class WebClientConfigType(Enum):
|
||||||
|
MAINSAIL: str = "mainsail-config"
|
||||||
|
FLUIDD: str = "fluidd-config"
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWebClient(ABC):
|
||||||
|
"""Base class for webclient data"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client(self) -> WebClientType:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def display_name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def backup_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def repo_path(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def download_url(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client_config(self) -> BaseWebClientConfig:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWebClientConfig(ABC):
|
||||||
|
"""Base class for webclient config data"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def client_config(self) -> WebClientConfigType:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def display_name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def config_filename(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def config_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def backup_dir(self) -> Path:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def repo_url(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def config_section(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import BaseWebClientConfig
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.config_utils import remove_config_section
|
||||||
|
from utils.fs_utils import remove_file
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def run_client_config_removal(
|
||||||
|
client_config: BaseWebClientConfig,
|
||||||
|
kl_instances: List[Klipper],
|
||||||
|
mr_instances: List[Moonraker],
|
||||||
|
) -> None:
|
||||||
|
remove_client_config_dir(client_config)
|
||||||
|
remove_client_config_symlink(client_config)
|
||||||
|
remove_config_section(f"update_manager {client_config.name}", mr_instances)
|
||||||
|
remove_config_section(client_config.config_section, kl_instances)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_client_config_dir(client_config: BaseWebClientConfig) -> None:
|
||||||
|
Logger.print_status(f"Removing {client_config.name} ...")
|
||||||
|
client_config_dir = client_config.config_dir
|
||||||
|
if not client_config_dir.exists():
|
||||||
|
Logger.print_info(f"'{client_config_dir}' does not exist. Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(client_config_dir)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{client_config_dir}':\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_client_config_symlink(client_config: BaseWebClientConfig) -> None:
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
instances: List[Klipper] = im.instances
|
||||||
|
for instance in instances:
|
||||||
|
Logger.print_status(f"Removing symlink from '{instance.cfg_dir}' ...")
|
||||||
|
symlink = instance.cfg_dir.joinpath(client_config.config_filename)
|
||||||
|
if not symlink.is_symlink():
|
||||||
|
Logger.print_info(f"'{symlink}' does not exist. Skipping ...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
remove_file(symlink)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
Logger.print_error("Failed to remove symlink!")
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig
|
||||||
|
from components.webui_client.client_dialogs import (
|
||||||
|
print_client_already_installed_dialog,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
backup_client_config_data,
|
||||||
|
config_for_other_client_exist,
|
||||||
|
)
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.common import backup_printer_config_dir
|
||||||
|
from utils.config_utils import add_config_section, add_config_section_at_top
|
||||||
|
from utils.fs_utils import create_symlink
|
||||||
|
from utils.git_utils import git_clone_wrapper, git_pull_wrapper
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def install_client_config(client_data: BaseWebClient) -> None:
|
||||||
|
client_config: BaseWebClientConfig = client_data.client_config
|
||||||
|
display_name = client_config.display_name
|
||||||
|
|
||||||
|
if config_for_other_client_exist(client_data.client):
|
||||||
|
Logger.print_info("Another Client-Config is already installed! Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
if client_config.config_dir.exists():
|
||||||
|
print_client_already_installed_dialog(display_name)
|
||||||
|
if get_confirm(f"Re-install {display_name}?", allow_go_back=True):
|
||||||
|
shutil.rmtree(client_config.config_dir)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances = kl_im.instances
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_client_config(client_config)
|
||||||
|
create_client_config_symlink(client_config, kl_instances)
|
||||||
|
|
||||||
|
backup_printer_config_dir()
|
||||||
|
|
||||||
|
add_config_section(
|
||||||
|
section=f"update_manager {client_config.name}",
|
||||||
|
instances=mr_instances,
|
||||||
|
options=[
|
||||||
|
("type", "git_repo"),
|
||||||
|
("primary_branch", "master"),
|
||||||
|
("path", str(client_config.config_dir)),
|
||||||
|
("origin", str(client_config.repo_url)),
|
||||||
|
("managed_services", "klipper"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
add_config_section_at_top(client_config.config_section, kl_instances)
|
||||||
|
kl_im.restart_all_instance()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"{display_name} installation failed!\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_ok(f"{display_name} installation complete!", start="\n")
|
||||||
|
|
||||||
|
|
||||||
|
def download_client_config(client_config: BaseWebClientConfig) -> None:
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Downloading {client_config.display_name} ...")
|
||||||
|
repo = client_config.repo_url
|
||||||
|
target_dir = client_config.config_dir
|
||||||
|
git_clone_wrapper(repo, target_dir)
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error(f"Downloading {client_config.display_name} failed!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def update_client_config(client: BaseWebClient) -> None:
|
||||||
|
client_config: BaseWebClientConfig = client.client_config
|
||||||
|
|
||||||
|
Logger.print_status(f"Updating {client_config.display_name} ...")
|
||||||
|
|
||||||
|
if not client_config.config_dir.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"Unable to update {client_config.display_name}. Directory does not exist! Skipping ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = KiauhSettings()
|
||||||
|
if settings.get("kiauh", "backup_before_update"):
|
||||||
|
backup_client_config_data(client)
|
||||||
|
|
||||||
|
git_pull_wrapper(client_config.repo_url, client_config.config_dir)
|
||||||
|
|
||||||
|
Logger.print_ok(f"Successfully updated {client_config.display_name}.")
|
||||||
|
Logger.print_info("Restart Klipper to reload the configuration!")
|
||||||
|
|
||||||
|
|
||||||
|
def create_client_config_symlink(
|
||||||
|
client_config: BaseWebClientConfig, klipper_instances: List[Klipper] = None
|
||||||
|
) -> None:
|
||||||
|
if klipper_instances is None:
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
klipper_instances = kl_im.instances
|
||||||
|
|
||||||
|
Logger.print_status(f"Create symlink for {client_config.config_filename} ...")
|
||||||
|
source = Path(client_config.config_dir, client_config.config_filename)
|
||||||
|
for instance in klipper_instances:
|
||||||
|
target = instance.cfg_dir
|
||||||
|
Logger.print_status(f"Linking {source} to {target}")
|
||||||
|
try:
|
||||||
|
create_symlink(source, target)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
Logger.print_error("Creating symlink failed!")
|
||||||
108
kiauh/components/webui_client/client_dialogs.py
Normal file
108
kiauh/components/webui_client/client_dialogs.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.webui_client.base_data import BaseWebClient
|
||||||
|
from core.menus.base_menu import print_back_footer
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
def print_moonraker_not_found_dialog():
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}No local Moonraker installation was found!{RESET_FORMAT}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| It is possible to install Mainsail 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 correctly. |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_client_already_installed_dialog(name: str):
|
||||||
|
line1 = f"{COLOR_YELLOW}WARNING:{RESET_FORMAT}"
|
||||||
|
line2 = f"{COLOR_YELLOW}{name} seems to be already installed!{RESET_FORMAT}"
|
||||||
|
line3 = f"If you continue, your current {name}"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<63}|
|
||||||
|
| {line2:<63}|
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line3:<54}|
|
||||||
|
| installation will be overwritten. |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
print_back_footer()
|
||||||
|
|
||||||
|
|
||||||
|
def print_client_port_select_dialog(name: str, port: int, ports_in_use: List[int]):
|
||||||
|
port = f"{COLOR_CYAN}{port}{RESET_FORMAT}"
|
||||||
|
line1 = f"Please select the port, {name} should be served on."
|
||||||
|
line2 = f"In case you need {name} to be served on a specific"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {line1:<54}|
|
||||||
|
| If you are unsure what to select, hit Enter to apply |
|
||||||
|
| the suggested value of: {port:38} |
|
||||||
|
| |
|
||||||
|
| {line2:<54}|
|
||||||
|
| port, you can set it now. Make sure the port is not |
|
||||||
|
| used by any other application on your system! |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if len(ports_in_use) > 0:
|
||||||
|
dialog += "|-------------------------------------------------------|\n"
|
||||||
|
dialog += "| The following ports were found to be in use already: |\n"
|
||||||
|
for port in ports_in_use:
|
||||||
|
port = f"{COLOR_CYAN}● {port}{RESET_FORMAT}"
|
||||||
|
dialog += f"| {port:60} |\n"
|
||||||
|
|
||||||
|
dialog += "\\=======================================================/\n"
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_install_client_config_dialog(client: BaseWebClient):
|
||||||
|
name = client.display_name
|
||||||
|
url = client.client_config.repo_url.replace(".git", "")
|
||||||
|
line1 = f"have {name} fully functional and working."
|
||||||
|
line2 = f"The recommended macros for {name} can be seen here:"
|
||||||
|
dialog = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| It is recommended to use special macros in order to |
|
||||||
|
| {line1:<54}|
|
||||||
|
| |
|
||||||
|
| {line2:<54}|
|
||||||
|
| {url:<54}|
|
||||||
|
| |
|
||||||
|
| If you already use these macros skip this step. |
|
||||||
|
| Otherwise you should consider to answer with 'Y' to |
|
||||||
|
| download the recommended macros. |
|
||||||
|
\\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
print(dialog, end="")
|
||||||
74
kiauh/components/webui_client/client_remove.py
Normal file
74
kiauh/components/webui_client/client_remove.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClient,
|
||||||
|
WebClientType,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_config.client_config_remove import (
|
||||||
|
run_client_config_removal,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import backup_mainsail_config_json
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from utils.config_utils import remove_config_section
|
||||||
|
from utils.fs_utils import (
|
||||||
|
remove_nginx_config,
|
||||||
|
remove_nginx_logs,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def run_client_removal(
|
||||||
|
client: BaseWebClient,
|
||||||
|
rm_client: bool,
|
||||||
|
rm_client_config: bool,
|
||||||
|
backup_ms_config_json: bool,
|
||||||
|
) -> None:
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances: List[Klipper] = kl_im.instances
|
||||||
|
|
||||||
|
if backup_ms_config_json and client.client == WebClientType.MAINSAIL:
|
||||||
|
backup_mainsail_config_json()
|
||||||
|
|
||||||
|
if rm_client:
|
||||||
|
client_name = client.name
|
||||||
|
remove_client_dir(client)
|
||||||
|
remove_nginx_config(client_name)
|
||||||
|
remove_nginx_logs(client_name, kl_instances)
|
||||||
|
|
||||||
|
section = f"update_manager {client_name}"
|
||||||
|
remove_config_section(section, mr_instances)
|
||||||
|
|
||||||
|
if rm_client_config:
|
||||||
|
run_client_config_removal(
|
||||||
|
client.client_config,
|
||||||
|
kl_instances,
|
||||||
|
mr_instances,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_client_dir(client: BaseWebClient) -> None:
|
||||||
|
Logger.print_status(f"Removing {client.display_name} ...")
|
||||||
|
client_dir = client.client_dir
|
||||||
|
if not client.client_dir.exists():
|
||||||
|
Logger.print_info(f"'{client_dir}' does not exist. Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(client_dir)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to delete '{client_dir}':\n{e}")
|
||||||
197
kiauh/components/webui_client/client_setup.py
Normal file
197
kiauh/components/webui_client/client_setup.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from components.webui_client import MODULE_PATH
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClient,
|
||||||
|
BaseWebClientConfig,
|
||||||
|
WebClientType,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
backup_mainsail_config_json,
|
||||||
|
config_for_other_client_exist,
|
||||||
|
enable_mainsail_remotemode,
|
||||||
|
restore_mainsail_config_json,
|
||||||
|
symlink_webui_nginx_log,
|
||||||
|
)
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
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 (
|
||||||
|
copy_common_vars_nginx_cfg,
|
||||||
|
copy_upstream_nginx_cfg,
|
||||||
|
create_nginx_cfg,
|
||||||
|
get_next_free_port,
|
||||||
|
is_valid_port,
|
||||||
|
read_ports_from_nginx_configs,
|
||||||
|
unzip,
|
||||||
|
)
|
||||||
|
from utils.input_utils import get_confirm, get_number_input
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
cmd_sysctl_service,
|
||||||
|
download_file,
|
||||||
|
get_ipv4_addr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_client(client: BaseWebClient) -> None:
|
||||||
|
if client is None:
|
||||||
|
raise ValueError("Missing parameter client_data!")
|
||||||
|
|
||||||
|
if client.client_dir.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"{client.display_name} seems to be already installed! Skipped ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
|
||||||
|
enable_remotemode = False
|
||||||
|
if not mr_instances:
|
||||||
|
print_moonraker_not_found_dialog()
|
||||||
|
if not get_confirm(
|
||||||
|
f"Continue {client.display_name} installation?",
|
||||||
|
allow_go_back=True,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# if moonraker is not installed or multiple instances
|
||||||
|
# are installed we enable mainsails remote mode
|
||||||
|
if (
|
||||||
|
client.client == WebClientType.MAINSAIL
|
||||||
|
and not mr_instances
|
||||||
|
or len(mr_instances) > 1
|
||||||
|
):
|
||||||
|
enable_remotemode = True
|
||||||
|
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances = kl_im.instances
|
||||||
|
install_client_cfg = False
|
||||||
|
client_config: BaseWebClientConfig = client.client_config
|
||||||
|
if (
|
||||||
|
kl_instances
|
||||||
|
and not client_config.config_dir.exists()
|
||||||
|
and not config_for_other_client_exist(client_to_ignore=client.client)
|
||||||
|
):
|
||||||
|
print_install_client_config_dialog(client)
|
||||||
|
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)
|
||||||
|
|
||||||
|
check_install_dependencies(["nginx", "unzip"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_client(client)
|
||||||
|
if enable_remotemode and client.client == WebClientType.MAINSAIL:
|
||||||
|
enable_mainsail_remotemode()
|
||||||
|
if mr_instances:
|
||||||
|
add_config_section(
|
||||||
|
section=f"update_manager {client.name}",
|
||||||
|
instances=mr_instances,
|
||||||
|
options=[
|
||||||
|
("type", "web"),
|
||||||
|
("channel", "stable"),
|
||||||
|
("repo", str(client.repo_path)),
|
||||||
|
("path", str(client.client_dir)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
mr_im.restart_all_instance()
|
||||||
|
if install_client_cfg and kl_instances:
|
||||||
|
install_client_config(client)
|
||||||
|
|
||||||
|
copy_upstream_nginx_cfg()
|
||||||
|
copy_common_vars_nginx_cfg()
|
||||||
|
create_nginx_cfg(
|
||||||
|
display_name=client.display_name,
|
||||||
|
cfg_name=client.name,
|
||||||
|
template_src=MODULE_PATH.joinpath("assets/nginx_cfg"),
|
||||||
|
PORT=port,
|
||||||
|
ROOT_DIR=client.client_dir,
|
||||||
|
NAME=client.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if kl_instances:
|
||||||
|
symlink_webui_nginx_log(kl_instances)
|
||||||
|
cmd_sysctl_service("nginx", "restart")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"{client.display_name} installation failed!\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log = f"Open {client.display_name} now on: http://{get_ipv4_addr()}:{port}"
|
||||||
|
Logger.print_ok(f"{client.display_name} installation complete!", start="\n")
|
||||||
|
Logger.print_ok(log, prefix=False, end="\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def download_client(client: BaseWebClient) -> None:
|
||||||
|
zipfile = f"{client.name.lower()}.zip"
|
||||||
|
target = Path().home().joinpath(zipfile)
|
||||||
|
try:
|
||||||
|
Logger.print_status(
|
||||||
|
f"Downloading {client.display_name} from {client.download_url} ..."
|
||||||
|
)
|
||||||
|
download_file(client.download_url, target, True)
|
||||||
|
Logger.print_ok("Download complete!")
|
||||||
|
|
||||||
|
Logger.print_status(f"Extracting {zipfile} ...")
|
||||||
|
unzip(target, client.client_dir)
|
||||||
|
target.unlink(missing_ok=True)
|
||||||
|
Logger.print_ok("OK!")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
Logger.print_error(f"Downloading {client.display_name} failed!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def update_client(client: BaseWebClient) -> None:
|
||||||
|
Logger.print_status(f"Updating {client.display_name} ...")
|
||||||
|
if not client.client_dir.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
f"Unable to update {client.display_name}. Directory does not exist! Skipping ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if client.client == WebClientType.MAINSAIL:
|
||||||
|
backup_mainsail_config_json(is_temp=True)
|
||||||
|
|
||||||
|
download_client(client)
|
||||||
|
|
||||||
|
if client.client == WebClientType.MAINSAIL:
|
||||||
|
restore_mainsail_config_json()
|
||||||
230
kiauh/components/webui_client/client_utils.py
Normal file
230
kiauh/components/webui_client/client_utils.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 # noqa: I001
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, get_args
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClient,
|
||||||
|
BaseWebClientConfig,
|
||||||
|
WebClientType,
|
||||||
|
)
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils import NGINX_CONFD, NGINX_SITES_AVAILABLE
|
||||||
|
from utils.common import get_install_status
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
from utils.git_utils import (
|
||||||
|
get_latest_tag,
|
||||||
|
get_latest_unstable_tag,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.types import ComponentStatus, InstallStatus
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_status(
|
||||||
|
client: BaseWebClient, fetch_remote: bool = False
|
||||||
|
) -> ComponentStatus:
|
||||||
|
files = [
|
||||||
|
NGINX_SITES_AVAILABLE.joinpath(client.name),
|
||||||
|
NGINX_CONFD.joinpath("upstreams.conf"),
|
||||||
|
NGINX_CONFD.joinpath("common_vars.conf"),
|
||||||
|
]
|
||||||
|
status = get_install_status(client.client_dir, files=files)
|
||||||
|
|
||||||
|
# if the client dir does not exist, set the status to not
|
||||||
|
# installed even if the other files are present
|
||||||
|
if not client.client_dir.exists():
|
||||||
|
status["status"] = InstallStatus.NOT_INSTALLED
|
||||||
|
|
||||||
|
status["local"] = get_local_client_version(client)
|
||||||
|
status["remote"] = get_remote_client_version(client) if fetch_remote else None
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_config_status(client: BaseWebClient) -> ComponentStatus:
|
||||||
|
return get_install_status(client.client_config.config_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_client_config(clients: List[BaseWebClient]) -> str:
|
||||||
|
installed = []
|
||||||
|
for client in clients:
|
||||||
|
client_config = client.client_config
|
||||||
|
if client_config.config_dir.exists():
|
||||||
|
installed.append(client)
|
||||||
|
|
||||||
|
if len(installed) > 1:
|
||||||
|
return f"{COLOR_YELLOW}Conflict!{RESET_FORMAT}"
|
||||||
|
elif len(installed) == 1:
|
||||||
|
cfg = installed[0].client_config
|
||||||
|
return f"{COLOR_CYAN}{cfg.display_name}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
return f"{COLOR_CYAN}-{RESET_FORMAT}"
|
||||||
|
|
||||||
|
|
||||||
|
def backup_mainsail_config_json(is_temp=False) -> None:
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
Logger.print_status(f"Backup '{c_json}' ...")
|
||||||
|
bm = BackupManager()
|
||||||
|
if is_temp:
|
||||||
|
fn = Path.home().joinpath("config.json.kiauh.bak")
|
||||||
|
bm.backup_file(c_json, custom_filename=fn)
|
||||||
|
else:
|
||||||
|
bm.backup_file(c_json)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_mainsail_config_json() -> None:
|
||||||
|
try:
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
Logger.print_status(f"Restore '{c_json}' ...")
|
||||||
|
source = Path.home().joinpath("config.json.kiauh.bak")
|
||||||
|
shutil.copy(source, c_json)
|
||||||
|
except OSError:
|
||||||
|
Logger.print_info("Unable to restore config.json. Skipped ...")
|
||||||
|
|
||||||
|
|
||||||
|
def enable_mainsail_remotemode() -> None:
|
||||||
|
Logger.print_status("Enable Mainsails remote mode ...")
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
with open(c_json, "r") as f:
|
||||||
|
config_data = json.load(f)
|
||||||
|
|
||||||
|
if config_data["instancesDB"] == "browser":
|
||||||
|
Logger.print_info("Remote mode already configured. Skipped ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Setting instance storage location to 'browser' ...")
|
||||||
|
config_data["instancesDB"] = "browser"
|
||||||
|
|
||||||
|
with open(c_json, "w") as f:
|
||||||
|
json.dump(config_data, f, indent=4)
|
||||||
|
Logger.print_ok("Mainsails remote mode enabled!")
|
||||||
|
|
||||||
|
|
||||||
|
def symlink_webui_nginx_log(klipper_instances: List[Klipper]) -> None:
|
||||||
|
Logger.print_status("Link NGINX logs into log directory ...")
|
||||||
|
access_log = Path("/var/log/nginx/mainsail-access.log")
|
||||||
|
error_log = Path("/var/log/nginx/mainsail-error.log")
|
||||||
|
|
||||||
|
for instance in klipper_instances:
|
||||||
|
desti_access = instance.log_dir.joinpath("mainsail-access.log")
|
||||||
|
if not desti_access.exists():
|
||||||
|
desti_access.symlink_to(access_log)
|
||||||
|
|
||||||
|
desti_error = instance.log_dir.joinpath("mainsail-error.log")
|
||||||
|
if not desti_error.exists():
|
||||||
|
desti_error.symlink_to(error_log)
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_client_version(client: BaseWebClient) -> str:
|
||||||
|
relinfo_file = client.client_dir.joinpath("release_info.json")
|
||||||
|
version_file = client.client_dir.joinpath(".version")
|
||||||
|
|
||||||
|
if not client.client_dir.exists():
|
||||||
|
return "-"
|
||||||
|
if not relinfo_file.is_file() and not version_file.is_file():
|
||||||
|
return "n/a"
|
||||||
|
|
||||||
|
if relinfo_file.is_file():
|
||||||
|
with open(relinfo_file, "r") as f:
|
||||||
|
return json.load(f)["version"]
|
||||||
|
else:
|
||||||
|
with open(version_file, "r") as f:
|
||||||
|
return f.readlines()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_remote_client_version(client: BaseWebClient) -> str:
|
||||||
|
try:
|
||||||
|
if (tag := get_latest_tag(client.repo_path)) != "":
|
||||||
|
return tag
|
||||||
|
return "ERROR"
|
||||||
|
except Exception:
|
||||||
|
return "ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
def backup_client_data(client: BaseWebClient) -> None:
|
||||||
|
name = client.name
|
||||||
|
src = client.client_dir
|
||||||
|
dest = client.backup_dir
|
||||||
|
|
||||||
|
with open(src.joinpath(".version"), "r") as v:
|
||||||
|
version = v.readlines()[0]
|
||||||
|
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(f"{name}-{version}", src, dest)
|
||||||
|
if name == "mainsail":
|
||||||
|
c_json = MainsailData().client_dir.joinpath("config.json")
|
||||||
|
bm.backup_file(c_json, dest)
|
||||||
|
bm.backup_file(NGINX_SITES_AVAILABLE.joinpath(name), dest)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_client_config_data(client: BaseWebClient) -> None:
|
||||||
|
client_config = client.client_config
|
||||||
|
name = client_config.name
|
||||||
|
source = client_config.config_dir
|
||||||
|
target = client_config.backup_dir
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(name, source, target)
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_clients() -> List[BaseWebClient]:
|
||||||
|
clients = list(get_args(WebClientType))
|
||||||
|
installed_clients: List[BaseWebClient] = []
|
||||||
|
for client in clients:
|
||||||
|
if client.client_dir.exists():
|
||||||
|
installed_clients.append(client)
|
||||||
|
|
||||||
|
return installed_clients
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_client_config() -> List[BaseWebClient]:
|
||||||
|
clients = list(get_args(WebClientType))
|
||||||
|
installed_client_configs: List[BaseWebClient] = []
|
||||||
|
for client in clients:
|
||||||
|
c_config_data: BaseWebClientConfig = client.client_config
|
||||||
|
if c_config_data.config_dir.exists():
|
||||||
|
installed_client_configs.append(client)
|
||||||
|
|
||||||
|
return installed_client_configs
|
||||||
|
|
||||||
|
|
||||||
|
def config_for_other_client_exist(client_to_ignore: WebClientType) -> bool:
|
||||||
|
"""
|
||||||
|
Check if any other client configs are present on the system.
|
||||||
|
It is usually not harmful, but chances are they can conflict each other.
|
||||||
|
Multiple client configs are, at least, redundant to have them installed
|
||||||
|
:param client_to_ignore: The client name to ignore for the check
|
||||||
|
:return: True, if other client configs were found, else False
|
||||||
|
"""
|
||||||
|
|
||||||
|
clients = set([c.name for c in get_existing_client_config()])
|
||||||
|
clients = clients - {client_to_ignore.value}
|
||||||
|
|
||||||
|
return True if len(clients) > 0 else False
|
||||||
|
|
||||||
|
|
||||||
|
def get_download_url(base_url: str, client: BaseWebClient) -> str:
|
||||||
|
settings = KiauhSettings()
|
||||||
|
use_unstable = settings.get(client.name, "unstable_releases")
|
||||||
|
stable_url = f"{base_url}/latest/download/{client.name}.zip"
|
||||||
|
|
||||||
|
if not use_unstable:
|
||||||
|
return stable_url
|
||||||
|
|
||||||
|
try:
|
||||||
|
unstable_tag = get_latest_unstable_tag(client.repo_path)
|
||||||
|
if unstable_tag == "":
|
||||||
|
raise Exception
|
||||||
|
return f"{base_url}/download/{unstable_tag}/{client.name}.zip"
|
||||||
|
except Exception:
|
||||||
|
return stable_url
|
||||||
54
kiauh/components/webui_client/fluidd_data.py
Normal file
54
kiauh/components/webui_client/fluidd_data.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClient,
|
||||||
|
BaseWebClientConfig,
|
||||||
|
WebClientConfigType,
|
||||||
|
WebClientType,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import get_download_url
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FluiddConfigWeb(BaseWebClientConfig):
|
||||||
|
client_config: WebClientConfigType = WebClientConfigType.FLUIDD
|
||||||
|
name: str = client_config.value
|
||||||
|
display_name: str = name.title()
|
||||||
|
config_dir: Path = Path.home().joinpath("fluidd-config")
|
||||||
|
config_filename: str = "fluidd.cfg"
|
||||||
|
config_section: str = f"include {config_filename}"
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-config-backups")
|
||||||
|
repo_url: str = "https://github.com/fluidd-core/fluidd-config.git"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FluiddData(BaseWebClient):
|
||||||
|
BASE_DL_URL = "https://github.com/fluidd-core/fluidd/releases"
|
||||||
|
|
||||||
|
client: WebClientType = WebClientType.FLUIDD
|
||||||
|
name: str = client.value
|
||||||
|
display_name: str = name.capitalize()
|
||||||
|
client_dir: Path = Path.home().joinpath("fluidd")
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("fluidd-backups")
|
||||||
|
repo_path: str = "fluidd-core/fluidd"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def download_url(self) -> str:
|
||||||
|
return get_download_url(self.BASE_DL_URL, self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_config(self) -> BaseWebClientConfig:
|
||||||
|
return FluiddConfigWeb()
|
||||||
55
kiauh/components/webui_client/mainsail_data.py
Normal file
55
kiauh/components/webui_client/mainsail_data.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from components.webui_client.base_data import (
|
||||||
|
BaseWebClient,
|
||||||
|
BaseWebClientConfig,
|
||||||
|
WebClientConfigType,
|
||||||
|
WebClientType,
|
||||||
|
)
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MainsailConfigWeb(BaseWebClientConfig):
|
||||||
|
client_config: WebClientConfigType = WebClientConfigType.MAINSAIL
|
||||||
|
name: str = client_config.value
|
||||||
|
display_name: str = name.title()
|
||||||
|
config_dir: Path = Path.home().joinpath("mainsail-config")
|
||||||
|
config_filename: str = "mainsail.cfg"
|
||||||
|
config_section: str = f"include {config_filename}"
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-config-backups")
|
||||||
|
repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MainsailData(BaseWebClient):
|
||||||
|
BASE_DL_URL: str = "https://github.com/mainsail-crew/mainsail/releases"
|
||||||
|
|
||||||
|
client: WebClientType = WebClientType.MAINSAIL
|
||||||
|
name: str = WebClientType.MAINSAIL.value
|
||||||
|
display_name: str = name.capitalize()
|
||||||
|
client_dir: Path = Path.home().joinpath("mainsail")
|
||||||
|
backup_dir: Path = BACKUP_ROOT_DIR.joinpath("mainsail-backups")
|
||||||
|
repo_path: str = "mainsail-crew/mainsail"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def download_url(self) -> str:
|
||||||
|
from components.webui_client.client_utils import get_download_url
|
||||||
|
|
||||||
|
return get_download_url(self.BASE_DL_URL, self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_config(self) -> BaseWebClientConfig:
|
||||||
|
return MainsailConfigWeb()
|
||||||
0
kiauh/components/webui_client/menus/__init__.py
Normal file
0
kiauh/components/webui_client/menus/__init__.py
Normal file
125
kiauh/components/webui_client/menus/client_remove_menu.py
Normal file
125
kiauh/components/webui_client/menus/client_remove_menu.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.webui_client import client_remove
|
||||||
|
from components.webui_client.base_data import BaseWebClient, WebClientType
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_RED, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
class ClientRemoveMenu(BaseMenu):
|
||||||
|
def __init__(
|
||||||
|
self, client: BaseWebClient, previous_menu: Optional[Type[BaseMenu]] = None
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.client = client
|
||||||
|
self.rm_client = False
|
||||||
|
self.rm_client_config = False
|
||||||
|
self.backup_mainsail_config_json = False
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else RemoveMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(method=self.toggle_all, menu=False),
|
||||||
|
"1": Option(method=self.toggle_rm_client, menu=False),
|
||||||
|
"2": Option(method=self.toggle_rm_client_config, menu=False),
|
||||||
|
"c": Option(method=self.run_removal_process, menu=False),
|
||||||
|
}
|
||||||
|
if self.client.client == WebClientType.MAINSAIL:
|
||||||
|
self.options["3"] = Option(self.toggle_backup_mainsail_config_json, False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
client_name = self.client.display_name
|
||||||
|
client_config = self.client.client_config
|
||||||
|
client_config_name = client_config.display_name
|
||||||
|
|
||||||
|
header = f" [ Remove {client_name} ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
checked = f"[{COLOR_CYAN}x{RESET_FORMAT}]"
|
||||||
|
unchecked = "[ ]"
|
||||||
|
o1 = checked if self.rm_client else unchecked
|
||||||
|
o2 = checked if self.rm_client_config else unchecked
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Enter a number and hit enter to select / deselect |
|
||||||
|
| the specific option for removal. |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Select everything |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) {o1} Remove {client_name:16} |
|
||||||
|
| 2) {o2} Remove {client_config_name:24} |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if self.client.client == WebClientType.MAINSAIL:
|
||||||
|
o3 = checked if self.backup_mainsail_config_json else unchecked
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
| 3) {o3} Backup config.json |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| C) Continue |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def toggle_all(self, **kwargs) -> None:
|
||||||
|
self.rm_client = True
|
||||||
|
self.rm_client_config = True
|
||||||
|
self.backup_mainsail_config_json = True
|
||||||
|
|
||||||
|
def toggle_rm_client(self, **kwargs) -> None:
|
||||||
|
self.rm_client = not self.rm_client
|
||||||
|
|
||||||
|
def toggle_rm_client_config(self, **kwargs) -> None:
|
||||||
|
self.rm_client_config = not self.rm_client_config
|
||||||
|
|
||||||
|
def toggle_backup_mainsail_config_json(self, **kwargs) -> None:
|
||||||
|
self.backup_mainsail_config_json = not self.backup_mainsail_config_json
|
||||||
|
|
||||||
|
def run_removal_process(self, **kwargs) -> None:
|
||||||
|
if (
|
||||||
|
not self.rm_client
|
||||||
|
and not self.rm_client_config
|
||||||
|
and not self.backup_mainsail_config_json
|
||||||
|
):
|
||||||
|
error = f"{COLOR_RED}Nothing selected ...{RESET_FORMAT}"
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
client_remove.run_client_removal(
|
||||||
|
client=self.client,
|
||||||
|
rm_client=self.rm_client,
|
||||||
|
rm_client_config=self.rm_client_config,
|
||||||
|
backup_ms_config_json=self.backup_mainsail_config_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rm_client = False
|
||||||
|
self.rm_client_config = False
|
||||||
|
self.backup_mainsail_config_json = False
|
||||||
0
kiauh/core/__init__.py
Normal file
0
kiauh/core/__init__.py
Normal file
12
kiauh/core/backup_manager/__init__.py
Normal file
12
kiauh/core/backup_manager/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BACKUP_ROOT_DIR = Path.home().joinpath("kiauh-backups")
|
||||||
91
kiauh/core/backup_manager/backup_manager.py
Normal file
91
kiauh/core/backup_manager/backup_manager.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
from utils.common import get_current_date
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BackupManager:
|
||||||
|
def __init__(self, backup_root_dir: Path = BACKUP_ROOT_DIR):
|
||||||
|
self._backup_root_dir = backup_root_dir
|
||||||
|
self._ignore_folders = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backup_root_dir(self) -> Path:
|
||||||
|
return self._backup_root_dir
|
||||||
|
|
||||||
|
@backup_root_dir.setter
|
||||||
|
def backup_root_dir(self, value: Path):
|
||||||
|
self._backup_root_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_folders(self) -> List[str]:
|
||||||
|
return self._ignore_folders
|
||||||
|
|
||||||
|
@ignore_folders.setter
|
||||||
|
def ignore_folders(self, value: List[str]):
|
||||||
|
self._ignore_folders = value
|
||||||
|
|
||||||
|
def backup_file(self, file: Path, target: Path = None, custom_filename=None):
|
||||||
|
Logger.print_status(f"Creating backup of {file} ...")
|
||||||
|
|
||||||
|
if not file.exists():
|
||||||
|
Logger.print_info("File does not exist! Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
target = self.backup_root_dir if target is None else target
|
||||||
|
|
||||||
|
if Path(file).is_file():
|
||||||
|
date = get_current_date().get("date")
|
||||||
|
time = get_current_date().get("time")
|
||||||
|
filename = f"{file.stem}-{date}-{time}{file.suffix}"
|
||||||
|
filename = custom_filename if custom_filename is not None else filename
|
||||||
|
try:
|
||||||
|
Path(target).mkdir(exist_ok=True)
|
||||||
|
shutil.copyfile(file, target.joinpath(filename))
|
||||||
|
Logger.print_ok("Backup successful!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to backup '{file}':\n{e}")
|
||||||
|
else:
|
||||||
|
Logger.print_info(f"File '{file}' not found ...")
|
||||||
|
|
||||||
|
def backup_directory(self, name: str, source: Path, target: Path = None) -> None:
|
||||||
|
Logger.print_status(f"Creating backup of {name} in {target} ...")
|
||||||
|
|
||||||
|
if source is None or not Path(source).exists():
|
||||||
|
Logger.print_info("Source directory does not exist! Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
target = self.backup_root_dir if target is None else target
|
||||||
|
try:
|
||||||
|
date = get_current_date().get("date")
|
||||||
|
time = get_current_date().get("time")
|
||||||
|
shutil.copytree(
|
||||||
|
source,
|
||||||
|
target.joinpath(f"{name.lower()}-{date}-{time}"),
|
||||||
|
ignore=self.ignore_folders_func,
|
||||||
|
)
|
||||||
|
Logger.print_ok("Backup successful!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to backup directory '{source}':\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def ignore_folders_func(self, dirpath, filenames):
|
||||||
|
return (
|
||||||
|
[f for f in filenames if f in self._ignore_folders]
|
||||||
|
if self._ignore_folders is not None
|
||||||
|
else []
|
||||||
|
)
|
||||||
0
kiauh/core/config_manager/__init__.py
Normal file
0
kiauh/core/config_manager/__init__.py
Normal file
83
kiauh/core/config_manager/config_manager.py
Normal file
83
kiauh/core/config_manager/config_manager.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 configparser
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class ConfigManager:
|
||||||
|
def __init__(self, cfg_file: Path):
|
||||||
|
self.config_file = cfg_file
|
||||||
|
self.config = CustomConfigParser()
|
||||||
|
|
||||||
|
if cfg_file.is_file():
|
||||||
|
self.read_config()
|
||||||
|
|
||||||
|
def read_config(self) -> None:
|
||||||
|
if not self.config_file:
|
||||||
|
Logger.print_error("Unable to read config file. File not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.config.read_file(open(self.config_file, "r"))
|
||||||
|
|
||||||
|
def write_config(self) -> None:
|
||||||
|
with open(self.config_file, "w") as cfg:
|
||||||
|
self.config.write(cfg)
|
||||||
|
|
||||||
|
def get_value(self, section: str, key: str, silent=True) -> Union[str, bool, None]:
|
||||||
|
if not self.config.has_section(section):
|
||||||
|
if not silent:
|
||||||
|
log = f"Section not defined. Unable to read section: [{section}]."
|
||||||
|
Logger.print_error(log)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self.config.has_option(section, key):
|
||||||
|
if not silent:
|
||||||
|
log = f"Option not defined in section [{section}]. Unable to read option: '{key}'."
|
||||||
|
Logger.print_error(log)
|
||||||
|
return None
|
||||||
|
|
||||||
|
value = self.config.get(section, key)
|
||||||
|
if value == "True" or value == "true":
|
||||||
|
return True
|
||||||
|
elif value == "False" or value == "false":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set_value(self, section: str, key: str, value: str):
|
||||||
|
self.config.set(section, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomConfigParser(configparser.ConfigParser):
|
||||||
|
"""
|
||||||
|
A custom ConfigParser class overwriting the write() method of configparser.Configparser.
|
||||||
|
Key and value will be delimited by a ": ".
|
||||||
|
Note the whitespace AFTER the colon, which is the whole reason for that overwrite.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def write(self, fp, space_around_delimiters=False):
|
||||||
|
if self._defaults:
|
||||||
|
fp.write("[%s]\n" % configparser.DEFAULTSECT)
|
||||||
|
for key, value in self._defaults.items():
|
||||||
|
fp.write("%s: %s\n" % (key, str(value).replace("\n", "\n\t")))
|
||||||
|
fp.write("\n")
|
||||||
|
for section in self._sections:
|
||||||
|
fp.write("[%s]\n" % section)
|
||||||
|
for key, value in self._sections[section].items():
|
||||||
|
if key == "__name__":
|
||||||
|
continue
|
||||||
|
if (value is not None) or (self._optcre == self.OPTCRE):
|
||||||
|
key = ": ".join((key, str(value).replace("\n", "\n\t")))
|
||||||
|
fp.write("%s\n" % key)
|
||||||
|
fp.write("\n")
|
||||||
0
kiauh/core/instance_manager/__init__.py
Normal file
0
kiauh/core/instance_manager/__init__.py
Normal file
161
kiauh/core/instance_manager/base_instance.py
Normal file
161
kiauh/core/instance_manager/base_instance.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from utils.constants import CURRENT_USER, SYSTEMD
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInstance(ABC):
|
||||||
|
@classmethod
|
||||||
|
def blacklist(cls) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
suffix: str,
|
||||||
|
instance_type: BaseInstance,
|
||||||
|
):
|
||||||
|
self._instance_type = instance_type
|
||||||
|
self._suffix = suffix
|
||||||
|
self._user = CURRENT_USER
|
||||||
|
self._data_dir_name = self.get_data_dir_name_from_suffix()
|
||||||
|
self._data_dir = Path.home().joinpath(f"{self._data_dir_name}_data")
|
||||||
|
self._cfg_dir = self.data_dir.joinpath("config")
|
||||||
|
self._log_dir = self.data_dir.joinpath("logs")
|
||||||
|
self._comms_dir = self.data_dir.joinpath("comms")
|
||||||
|
self._sysd_dir = self.data_dir.joinpath("systemd")
|
||||||
|
self._gcodes_dir = self.data_dir.joinpath("gcodes")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_type(self) -> BaseInstance:
|
||||||
|
return self._instance_type
|
||||||
|
|
||||||
|
@instance_type.setter
|
||||||
|
def instance_type(self, value: BaseInstance) -> None:
|
||||||
|
self._instance_type = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suffix(self) -> str:
|
||||||
|
return self._suffix
|
||||||
|
|
||||||
|
@suffix.setter
|
||||||
|
def suffix(self, value: str) -> None:
|
||||||
|
self._suffix = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self) -> str:
|
||||||
|
return self._user
|
||||||
|
|
||||||
|
@user.setter
|
||||||
|
def user(self, value: str) -> None:
|
||||||
|
self._user = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_dir_name(self) -> str:
|
||||||
|
return self._data_dir_name
|
||||||
|
|
||||||
|
@data_dir_name.setter
|
||||||
|
def data_dir_name(self, value: str) -> None:
|
||||||
|
self._data_dir_name = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_dir(self) -> Path:
|
||||||
|
return self._data_dir
|
||||||
|
|
||||||
|
@data_dir.setter
|
||||||
|
def data_dir(self, value: Path) -> None:
|
||||||
|
self._data_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cfg_dir(self) -> Path:
|
||||||
|
return self._cfg_dir
|
||||||
|
|
||||||
|
@cfg_dir.setter
|
||||||
|
def cfg_dir(self, value: Path) -> None:
|
||||||
|
self._cfg_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_dir(self) -> Path:
|
||||||
|
return self._log_dir
|
||||||
|
|
||||||
|
@log_dir.setter
|
||||||
|
def log_dir(self, value: Path) -> None:
|
||||||
|
self._log_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comms_dir(self) -> Path:
|
||||||
|
return self._comms_dir
|
||||||
|
|
||||||
|
@comms_dir.setter
|
||||||
|
def comms_dir(self, value: Path) -> None:
|
||||||
|
self._comms_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sysd_dir(self) -> Path:
|
||||||
|
return self._sysd_dir
|
||||||
|
|
||||||
|
@sysd_dir.setter
|
||||||
|
def sysd_dir(self, value: Path) -> None:
|
||||||
|
self._sysd_dir = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gcodes_dir(self) -> Path:
|
||||||
|
return self._gcodes_dir
|
||||||
|
|
||||||
|
@gcodes_dir.setter
|
||||||
|
def gcodes_dir(self, value: Path) -> None:
|
||||||
|
self._gcodes_dir = value
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create(self) -> None:
|
||||||
|
raise NotImplementedError("Subclasses must implement the create method")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(self) -> None:
|
||||||
|
raise NotImplementedError("Subclasses must implement the delete method")
|
||||||
|
|
||||||
|
def create_folders(self, add_dirs: Optional[List[Path]] = None) -> None:
|
||||||
|
dirs = [
|
||||||
|
self.data_dir,
|
||||||
|
self.cfg_dir,
|
||||||
|
self.log_dir,
|
||||||
|
self.comms_dir,
|
||||||
|
self.sysd_dir,
|
||||||
|
]
|
||||||
|
|
||||||
|
if add_dirs:
|
||||||
|
dirs.extend(add_dirs)
|
||||||
|
|
||||||
|
for _dir in dirs:
|
||||||
|
_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def get_service_file_name(self, extension: bool = False) -> str:
|
||||||
|
from utils.common import convert_camelcase_to_kebabcase
|
||||||
|
|
||||||
|
name = convert_camelcase_to_kebabcase(self.__class__.__name__)
|
||||||
|
if self.suffix != "":
|
||||||
|
name += f"-{self.suffix}"
|
||||||
|
|
||||||
|
return name if not extension else f"{name}.service"
|
||||||
|
|
||||||
|
def get_service_file_path(self) -> Path:
|
||||||
|
return SYSTEMD.joinpath(self.get_service_file_name(extension=True))
|
||||||
|
|
||||||
|
def get_data_dir_name_from_suffix(self) -> str:
|
||||||
|
if self._suffix == "":
|
||||||
|
return "printer"
|
||||||
|
elif self._suffix.isdigit():
|
||||||
|
return f"printer_{self._suffix}"
|
||||||
|
else:
|
||||||
|
return self._suffix
|
||||||
188
kiauh/core/instance_manager/instance_manager.py
Normal file
188
kiauh/core/instance_manager/instance_manager.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, TypeVar, Union
|
||||||
|
|
||||||
|
from core.instance_manager.base_instance import BaseInstance
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import cmd_sysctl_service
|
||||||
|
|
||||||
|
T = TypeVar(name="T", bound=BaseInstance, covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class InstanceManager:
|
||||||
|
def __init__(self, instance_type: T) -> None:
|
||||||
|
self._instance_type = instance_type
|
||||||
|
self._current_instance: Optional[T] = None
|
||||||
|
self._instance_suffix: Optional[str] = None
|
||||||
|
self._instance_service: Optional[str] = None
|
||||||
|
self._instance_service_full: Optional[str] = None
|
||||||
|
self._instance_service_path: Optional[str] = None
|
||||||
|
self._instances: List[T] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_type(self) -> T:
|
||||||
|
return self._instance_type
|
||||||
|
|
||||||
|
@instance_type.setter
|
||||||
|
def instance_type(self, value: T):
|
||||||
|
self._instance_type = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_instance(self) -> T:
|
||||||
|
return self._current_instance
|
||||||
|
|
||||||
|
@current_instance.setter
|
||||||
|
def current_instance(self, value: T) -> None:
|
||||||
|
self._current_instance = value
|
||||||
|
self.instance_suffix = value.suffix
|
||||||
|
self.instance_service = value.get_service_file_name()
|
||||||
|
self.instance_service_path = value.get_service_file_path()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_suffix(self) -> str:
|
||||||
|
return self._instance_suffix
|
||||||
|
|
||||||
|
@instance_suffix.setter
|
||||||
|
def instance_suffix(self, value: str):
|
||||||
|
self._instance_suffix = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_service(self) -> str:
|
||||||
|
return self._instance_service
|
||||||
|
|
||||||
|
@instance_service.setter
|
||||||
|
def instance_service(self, value: str):
|
||||||
|
self._instance_service = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_service_full(self) -> str:
|
||||||
|
return f"{self._instance_service}.service"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance_service_path(self) -> str:
|
||||||
|
return self._instance_service_path
|
||||||
|
|
||||||
|
@instance_service_path.setter
|
||||||
|
def instance_service_path(self, value: str):
|
||||||
|
self._instance_service_path = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instances(self) -> List[T]:
|
||||||
|
return self.find_instances()
|
||||||
|
|
||||||
|
@instances.setter
|
||||||
|
def instances(self, value: List[T]):
|
||||||
|
self._instances = value
|
||||||
|
|
||||||
|
def create_instance(self) -> None:
|
||||||
|
if self.current_instance is not None:
|
||||||
|
try:
|
||||||
|
self.current_instance.create()
|
||||||
|
except (OSError, subprocess.CalledProcessError) as e:
|
||||||
|
Logger.print_error(f"Creating instance failed: {e}")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise ValueError("current_instance cannot be None")
|
||||||
|
|
||||||
|
def delete_instance(self) -> None:
|
||||||
|
if self.current_instance is not None:
|
||||||
|
try:
|
||||||
|
self.current_instance.delete()
|
||||||
|
except (OSError, subprocess.CalledProcessError) as e:
|
||||||
|
Logger.print_error(f"Removing instance failed: {e}")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise ValueError("current_instance cannot be None")
|
||||||
|
|
||||||
|
def enable_instance(self) -> None:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service(self.instance_service_full, "enable")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error enabling service {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def disable_instance(self) -> None:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service(self.instance_service_full, "disable")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error disabling {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def start_instance(self) -> None:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service(self.instance_service_full, "start")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error starting {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def restart_instance(self) -> None:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service(self.instance_service_full, "restart")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error restarting {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
|
||||||
|
def start_all_instance(self) -> None:
|
||||||
|
for instance in self.instances:
|
||||||
|
self.current_instance = instance
|
||||||
|
self.start_instance()
|
||||||
|
|
||||||
|
def restart_all_instance(self) -> None:
|
||||||
|
for instance in self.instances:
|
||||||
|
self.current_instance = instance
|
||||||
|
self.restart_instance()
|
||||||
|
|
||||||
|
def stop_instance(self) -> None:
|
||||||
|
try:
|
||||||
|
cmd_sysctl_service(self.instance_service_full, "stop")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error stopping {self.instance_service_full}:")
|
||||||
|
Logger.print_error(f"{e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def stop_all_instance(self) -> None:
|
||||||
|
for instance in self.instances:
|
||||||
|
self.current_instance = instance
|
||||||
|
self.stop_instance()
|
||||||
|
|
||||||
|
def find_instances(self) -> List[T]:
|
||||||
|
from utils.common import convert_camelcase_to_kebabcase
|
||||||
|
|
||||||
|
name = convert_camelcase_to_kebabcase(self.instance_type.__name__)
|
||||||
|
pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$")
|
||||||
|
excluded = self.instance_type.blacklist()
|
||||||
|
|
||||||
|
service_list = [
|
||||||
|
Path(SYSTEMD, service)
|
||||||
|
for service in SYSTEMD.iterdir()
|
||||||
|
if pattern.search(service.name)
|
||||||
|
and not any(s in service.name for s in excluded)
|
||||||
|
]
|
||||||
|
|
||||||
|
instance_list = [
|
||||||
|
self.instance_type(suffix=self._get_instance_suffix(service))
|
||||||
|
for service in service_list
|
||||||
|
]
|
||||||
|
|
||||||
|
return sorted(instance_list, key=lambda x: self._sort_instance_list(x.suffix))
|
||||||
|
|
||||||
|
def _get_instance_suffix(self, file_path: Path) -> str:
|
||||||
|
return file_path.stem.split("-")[-1] if "-" in file_path.stem else ""
|
||||||
|
|
||||||
|
def _sort_instance_list(self, s: Union[int, str, None]):
|
||||||
|
if s is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
return int(s) if s.isdigit() else s
|
||||||
8
kiauh/core/instance_manager/name_scheme.py
Normal file
8
kiauh/core/instance_manager/name_scheme.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class NameScheme(Enum):
|
||||||
|
SINGLE = "SINGLE"
|
||||||
|
INDEX = "INDEX"
|
||||||
|
CUSTOM = "CUSTOM"
|
||||||
35
kiauh/core/menus/__init__.py
Normal file
35
kiauh/core/menus/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Callable, Union
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Option:
|
||||||
|
"""
|
||||||
|
Represents a menu option.
|
||||||
|
:param method: Method that will be used to call the menu option
|
||||||
|
:param menu: Flag for singaling that another menu will be opened
|
||||||
|
:param opt_index: Can be used to pass the user input to the menu option
|
||||||
|
:param opt_data: Can be used to pass any additional data to the menu option
|
||||||
|
"""
|
||||||
|
|
||||||
|
method: Union[Callable, None] = None
|
||||||
|
menu: bool = False
|
||||||
|
opt_index: str = ""
|
||||||
|
opt_data: Any = None
|
||||||
|
|
||||||
|
|
||||||
|
class FooterType(Enum):
|
||||||
|
QUIT = "QUIT"
|
||||||
|
BACK = "BACK"
|
||||||
|
BACK_HELP = "BACK_HELP"
|
||||||
|
BLANK = "BLANK"
|
||||||
96
kiauh/core/menus/advanced_menu.py
Normal file
96
kiauh/core/menus/advanced_menu.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipper_firmware.menus.klipper_build_menu import (
|
||||||
|
KlipperBuildFirmwareMenu,
|
||||||
|
)
|
||||||
|
from components.klipper_firmware.menus.klipper_flash_menu import (
|
||||||
|
KlipperFlashMethodMenu,
|
||||||
|
KlipperSelectMcuConnectionMenu,
|
||||||
|
)
|
||||||
|
from components.moonraker import MOONRAKER_DIR
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_YELLOW, RESET_FORMAT
|
||||||
|
from utils.git_utils import rollback_repository
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class AdvancedMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self):
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.klipper_rollback, menu=True),
|
||||||
|
"2": Option(method=self.moonraker_rollback, menu=True),
|
||||||
|
"3": Option(method=self.build, menu=True),
|
||||||
|
"4": Option(method=self.flash, menu=False),
|
||||||
|
"5": Option(method=self.build_flash, menu=False),
|
||||||
|
"6": Option(method=self.get_id, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Advanced Menu ] "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Repo Rollback: |
|
||||||
|
| 1) [Klipper] |
|
||||||
|
| 2) [Moonraker] |
|
||||||
|
| |
|
||||||
|
| Klipper Firmware: |
|
||||||
|
| 3) [Build] |
|
||||||
|
| 4) [Flash] |
|
||||||
|
| 5) [Build + Flash] |
|
||||||
|
| 6) [Get MCU ID] |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def klipper_rollback(self, **kwargs):
|
||||||
|
rollback_repository(KLIPPER_DIR, Klipper)
|
||||||
|
|
||||||
|
def moonraker_rollback(self, **kwargs):
|
||||||
|
rollback_repository(MOONRAKER_DIR, Moonraker)
|
||||||
|
|
||||||
|
def build(self, **kwargs):
|
||||||
|
KlipperBuildFirmwareMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def flash(self, **kwargs):
|
||||||
|
KlipperFlashMethodMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def build_flash(self, **kwargs):
|
||||||
|
KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run()
|
||||||
|
KlipperFlashMethodMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def get_id(self, **kwargs):
|
||||||
|
KlipperSelectMcuConnectionMenu(
|
||||||
|
previous_menu=self.__class__,
|
||||||
|
standalone=True,
|
||||||
|
).run()
|
||||||
108
kiauh/core/menus/backup_menu.py
Normal file
108
kiauh/core/menus/backup_menu.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.klipper.klipper_utils import backup_klipper_dir
|
||||||
|
from components.klipperscreen.klipperscreen import backup_klipperscreen_dir
|
||||||
|
from components.moonraker.moonraker_utils import (
|
||||||
|
backup_moonraker_db_dir,
|
||||||
|
backup_moonraker_dir,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
backup_client_config_data,
|
||||||
|
backup_client_data,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.common import backup_printer_config_dir
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BackupMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.backup_klipper, menu=False),
|
||||||
|
"2": Option(method=self.backup_moonraker, menu=False),
|
||||||
|
"3": Option(method=self.backup_printer_config, menu=False),
|
||||||
|
"4": Option(method=self.backup_moonraker_db, menu=False),
|
||||||
|
"5": Option(method=self.backup_mainsail, menu=False),
|
||||||
|
"6": Option(method=self.backup_fluidd, menu=False),
|
||||||
|
"7": Option(method=self.backup_mainsail_config, menu=False),
|
||||||
|
"8": Option(method=self.backup_fluidd_config, menu=False),
|
||||||
|
"9": Option(method=self.backup_klipperscreen, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Backup Menu ] "
|
||||||
|
line1 = f"{COLOR_YELLOW}INFO: Backups are located in '~/kiauh-backups'{RESET_FORMAT}"
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:^62} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Klipper & Moonraker API: | Client-Config: |
|
||||||
|
| 1) [Klipper] | 7) [Mainsail-Config] |
|
||||||
|
| 2) [Moonraker] | 8) [Fluidd-Config] |
|
||||||
|
| 3) [Config Folder] | |
|
||||||
|
| 4) [Moonraker Database] | Touchscreen GUI: |
|
||||||
|
| | 9) [KlipperScreen] |
|
||||||
|
| Webinterface: | |
|
||||||
|
| 5) [Mainsail] | |
|
||||||
|
| 6) [Fluidd] | |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def backup_klipper(self, **kwargs):
|
||||||
|
backup_klipper_dir()
|
||||||
|
|
||||||
|
def backup_moonraker(self, **kwargs):
|
||||||
|
backup_moonraker_dir()
|
||||||
|
|
||||||
|
def backup_printer_config(self, **kwargs):
|
||||||
|
backup_printer_config_dir()
|
||||||
|
|
||||||
|
def backup_moonraker_db(self, **kwargs):
|
||||||
|
backup_moonraker_db_dir()
|
||||||
|
|
||||||
|
def backup_mainsail(self, **kwargs):
|
||||||
|
backup_client_data(MainsailData())
|
||||||
|
|
||||||
|
def backup_fluidd(self, **kwargs):
|
||||||
|
backup_client_data(FluiddData())
|
||||||
|
|
||||||
|
def backup_mainsail_config(self, **kwargs):
|
||||||
|
backup_client_config_data(MainsailData())
|
||||||
|
|
||||||
|
def backup_fluidd_config(self, **kwargs):
|
||||||
|
backup_client_config_data(FluiddData())
|
||||||
|
|
||||||
|
def backup_klipperscreen(self, **kwargs):
|
||||||
|
backup_klipperscreen_dir()
|
||||||
219
kiauh/core/menus/base_menu.py
Normal file
219
kiauh/core/menus/base_menu.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import traceback
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import Dict, Optional, Type
|
||||||
|
|
||||||
|
from core.menus import FooterType, Option
|
||||||
|
from utils.constants import (
|
||||||
|
COLOR_CYAN,
|
||||||
|
COLOR_GREEN,
|
||||||
|
COLOR_RED,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
RESET_FORMAT,
|
||||||
|
)
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
def clear():
|
||||||
|
subprocess.call("clear", shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def print_header():
|
||||||
|
line1 = " [ KIAUH ] "
|
||||||
|
line2 = "Klipper Installation And Update Helper"
|
||||||
|
line3 = ""
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
header = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{line1:~^{count}}{RESET_FORMAT} |
|
||||||
|
| {color}{line2:^{count}}{RESET_FORMAT} |
|
||||||
|
| {color}{line3:~^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(header, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_quit_footer():
|
||||||
|
text = "Q) Quit"
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
footer = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {color}{text:^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(footer, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_back_footer():
|
||||||
|
text = "B) « Back"
|
||||||
|
color = COLOR_GREEN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
footer = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {color}{text:^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(footer, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_back_help_footer():
|
||||||
|
text1 = "B) « Back"
|
||||||
|
text2 = "H) Help [?]"
|
||||||
|
color1 = COLOR_GREEN
|
||||||
|
color2 = COLOR_YELLOW
|
||||||
|
count = 34 - len(color1) - len(RESET_FORMAT)
|
||||||
|
footer = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {color1}{text1:^{count}}{RESET_FORMAT} | {color2}{text2:^{count}}{RESET_FORMAT} |
|
||||||
|
\=======================================================/
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(footer, end="")
|
||||||
|
|
||||||
|
|
||||||
|
def print_blank_footer():
|
||||||
|
print("\=======================================================/")
|
||||||
|
|
||||||
|
|
||||||
|
class PostInitCaller(type):
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
obj = type.__call__(cls, *args, **kwargs)
|
||||||
|
obj.__post_init__()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BaseMenu(metaclass=PostInitCaller):
|
||||||
|
options: Dict[str, Option] = {}
|
||||||
|
options_offset: int = 0
|
||||||
|
default_option: Option = None
|
||||||
|
input_label_txt: str = "Perform action"
|
||||||
|
header: bool = False
|
||||||
|
previous_menu: Type[BaseMenu] = None
|
||||||
|
help_menu: Type[BaseMenu] = None
|
||||||
|
footer_type: FooterType = FooterType.BACK
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if type(self) is BaseMenu:
|
||||||
|
raise NotImplementedError("BaseMenu cannot be instantiated directly.")
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.set_previous_menu(self.previous_menu)
|
||||||
|
self.set_options()
|
||||||
|
|
||||||
|
# conditionally add options based on footer type
|
||||||
|
if self.footer_type is FooterType.QUIT:
|
||||||
|
self.options["q"] = Option(method=self.__exit, menu=False)
|
||||||
|
if self.footer_type is FooterType.BACK:
|
||||||
|
self.options["b"] = Option(method=self.__go_back, menu=False)
|
||||||
|
if self.footer_type is FooterType.BACK_HELP:
|
||||||
|
self.options["b"] = Option(method=self.__go_back, menu=False)
|
||||||
|
self.options["h"] = Option(method=self.__go_to_help, menu=False)
|
||||||
|
# if defined, add the default option to the options dict
|
||||||
|
if self.default_option is not None:
|
||||||
|
self.options[""] = self.default_option
|
||||||
|
|
||||||
|
def __go_back(self, **kwargs):
|
||||||
|
self.previous_menu().run()
|
||||||
|
|
||||||
|
def __go_to_help(self, **kwargs):
|
||||||
|
self.help_menu(previous_menu=self).run()
|
||||||
|
|
||||||
|
def __exit(self, **kwargs):
|
||||||
|
Logger.print_ok("###### Happy printing!", False)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_options(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def print_footer(self) -> None:
|
||||||
|
if self.footer_type is FooterType.QUIT:
|
||||||
|
print_quit_footer()
|
||||||
|
elif self.footer_type is FooterType.BACK:
|
||||||
|
print_back_footer()
|
||||||
|
elif self.footer_type is FooterType.BACK_HELP:
|
||||||
|
print_back_help_footer()
|
||||||
|
elif self.footer_type is FooterType.BLANK:
|
||||||
|
print_blank_footer()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def display_menu(self) -> None:
|
||||||
|
if self.header:
|
||||||
|
print_header()
|
||||||
|
self.print_menu()
|
||||||
|
self.print_footer()
|
||||||
|
|
||||||
|
def validate_user_input(self, usr_input: str) -> Option:
|
||||||
|
"""
|
||||||
|
Validate the user input and either return an Option, a string or None
|
||||||
|
:param usr_input: The user input in form of a string
|
||||||
|
:return: Option, str or None
|
||||||
|
"""
|
||||||
|
usr_input = usr_input.lower()
|
||||||
|
option = self.options.get(usr_input, Option(None, False, "", None))
|
||||||
|
|
||||||
|
# if option/usr_input is None/empty string, we execute the menus default option if specified
|
||||||
|
if (option is None or usr_input == "") and self.default_option is not None:
|
||||||
|
self.default_option.opt_index = usr_input
|
||||||
|
return self.default_option
|
||||||
|
|
||||||
|
# user selected a regular option
|
||||||
|
option.opt_index = usr_input
|
||||||
|
return option
|
||||||
|
|
||||||
|
def handle_user_input(self) -> Option:
|
||||||
|
"""Handle the user input, return the validated input or print an error."""
|
||||||
|
while True:
|
||||||
|
print(f"{COLOR_CYAN}###### {self.input_label_txt}: {RESET_FORMAT}", end="")
|
||||||
|
usr_input = input().lower()
|
||||||
|
validated_input = self.validate_user_input(usr_input)
|
||||||
|
|
||||||
|
if validated_input.method is not None:
|
||||||
|
return validated_input
|
||||||
|
else:
|
||||||
|
Logger.print_error("Invalid input!", False)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""Start the menu lifecycle. When this function returns, the lifecycle of the menu ends."""
|
||||||
|
try:
|
||||||
|
self.display_menu()
|
||||||
|
option = self.handle_user_input()
|
||||||
|
option.method(opt_index=option.opt_index, opt_data=option.opt_data)
|
||||||
|
self.run()
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(
|
||||||
|
f"An unexpected error occured:\n{e}\n{traceback.format_exc()}"
|
||||||
|
)
|
||||||
104
kiauh/core/menus/install_menu.py
Normal file
104
kiauh/core/menus/install_menu.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.crowsnest.crowsnest import install_crowsnest
|
||||||
|
from components.klipper import klipper_setup
|
||||||
|
from components.klipperscreen.klipperscreen import install_klipperscreen
|
||||||
|
from components.mobileraker.mobileraker import install_mobileraker
|
||||||
|
from components.moonraker import moonraker_setup
|
||||||
|
from components.webui_client import client_setup
|
||||||
|
from components.webui_client.client_config import client_config_setup
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_GREEN, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class InstallMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.install_klipper, menu=False),
|
||||||
|
"2": Option(method=self.install_moonraker, menu=False),
|
||||||
|
"3": Option(method=self.install_mainsail, menu=False),
|
||||||
|
"4": Option(method=self.install_fluidd, menu=False),
|
||||||
|
"5": Option(method=self.install_mainsail_config, menu=False),
|
||||||
|
"6": Option(method=self.install_fluidd_config, menu=False),
|
||||||
|
"7": Option(method=self.install_klipperscreen, menu=False),
|
||||||
|
"8": Option(method=self.install_mobileraker, menu=False),
|
||||||
|
"9": Option(method=self.install_crowsnest, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Installation Menu ] "
|
||||||
|
color = COLOR_GREEN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Firmware & API: | Touchscreen GUI: |
|
||||||
|
| 1) [Klipper] | 7) [KlipperScreen] |
|
||||||
|
| 2) [Moonraker] | |
|
||||||
|
| | Android / iOS: |
|
||||||
|
| Webinterface: | 8) [Mobileraker] |
|
||||||
|
| 3) [Mainsail] | |
|
||||||
|
| 4) [Fluidd] | Webcam Streamer: |
|
||||||
|
| | 9) [Crowsnest] |
|
||||||
|
| Client-Config: | |
|
||||||
|
| 5) [Mainsail-Config] | |
|
||||||
|
| 6) [Fluidd-Config] | |
|
||||||
|
| | |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def install_klipper(self, **kwargs):
|
||||||
|
klipper_setup.install_klipper()
|
||||||
|
|
||||||
|
def install_moonraker(self, **kwargs):
|
||||||
|
moonraker_setup.install_moonraker()
|
||||||
|
|
||||||
|
def install_mainsail(self, **kwargs):
|
||||||
|
client_setup.install_client(MainsailData())
|
||||||
|
|
||||||
|
def install_mainsail_config(self, **kwargs):
|
||||||
|
client_config_setup.install_client_config(MainsailData())
|
||||||
|
|
||||||
|
def install_fluidd(self, **kwargs):
|
||||||
|
client_setup.install_client(FluiddData())
|
||||||
|
|
||||||
|
def install_fluidd_config(self, **kwargs):
|
||||||
|
client_config_setup.install_client_config(FluiddData())
|
||||||
|
|
||||||
|
def install_klipperscreen(self, **kwargs):
|
||||||
|
install_klipperscreen()
|
||||||
|
|
||||||
|
def install_mobileraker(self, **kwargs):
|
||||||
|
install_mobileraker()
|
||||||
|
|
||||||
|
def install_crowsnest(self, **kwargs):
|
||||||
|
install_crowsnest()
|
||||||
172
kiauh/core/menus/main_menu.py
Normal file
172
kiauh/core/menus/main_menu.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
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
|
||||||
|
from components.log_uploads.menus.log_upload_menu import LogUploadMenu
|
||||||
|
from components.mobileraker.mobileraker import get_mobileraker_status
|
||||||
|
from components.moonraker.moonraker_utils import get_moonraker_status
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
get_client_status,
|
||||||
|
get_current_client_config,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import FooterType
|
||||||
|
from core.menus.advanced_menu import AdvancedMenu
|
||||||
|
from core.menus.backup_menu import BackupMenu
|
||||||
|
from core.menus.base_menu import BaseMenu, Option
|
||||||
|
from core.menus.install_menu import InstallMenu
|
||||||
|
from core.menus.remove_menu import RemoveMenu
|
||||||
|
from core.menus.settings_menu import SettingsMenu
|
||||||
|
from core.menus.update_menu import UpdateMenu
|
||||||
|
from extensions.extensions_menu import ExtensionsMenu
|
||||||
|
from utils.constants import (
|
||||||
|
COLOR_CYAN,
|
||||||
|
COLOR_GREEN,
|
||||||
|
COLOR_MAGENTA,
|
||||||
|
COLOR_RED,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
RESET_FORMAT,
|
||||||
|
)
|
||||||
|
from utils.types import ComponentStatus
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class MainMenu(BaseMenu):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.header = True
|
||||||
|
self.footer_type = FooterType.QUIT
|
||||||
|
|
||||||
|
self.kl_status = self.kl_repo = self.mr_status = self.mr_repo = ""
|
||||||
|
self.ms_status = self.fl_status = self.ks_status = self.mb_status = ""
|
||||||
|
self.cn_status = self.cc_status = ""
|
||||||
|
self.init_status()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
"""MainMenu does not have a previous menu"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(method=self.log_upload_menu, menu=True),
|
||||||
|
"1": Option(method=self.install_menu, menu=True),
|
||||||
|
"2": Option(method=self.update_menu, menu=True),
|
||||||
|
"3": Option(method=self.remove_menu, menu=True),
|
||||||
|
"4": Option(method=self.advanced_menu, menu=True),
|
||||||
|
"5": Option(method=self.backup_menu, menu=True),
|
||||||
|
"e": Option(method=self.extension_menu, menu=True),
|
||||||
|
"s": Option(method=self.settings_menu, menu=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
def init_status(self) -> None:
|
||||||
|
status_vars = ["kl", "mr", "ms", "fl", "ks", "mb", "cn"]
|
||||||
|
for var in status_vars:
|
||||||
|
setattr(
|
||||||
|
self,
|
||||||
|
f"{var}_status",
|
||||||
|
f"{COLOR_RED}Not installed!{RESET_FORMAT}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def fetch_status(self) -> None:
|
||||||
|
self._get_component_status("kl", get_klipper_status)
|
||||||
|
self._get_component_status("mr", get_moonraker_status)
|
||||||
|
self._get_component_status("ms", get_client_status, MainsailData())
|
||||||
|
self._get_component_status("fl", get_client_status, FluiddData())
|
||||||
|
self.cc_status = get_current_client_config([MainsailData(), FluiddData()])
|
||||||
|
self._get_component_status("ks", get_klipperscreen_status)
|
||||||
|
self._get_component_status("mb", get_mobileraker_status)
|
||||||
|
self._get_component_status("cn", get_crowsnest_status)
|
||||||
|
|
||||||
|
def _get_component_status(self, name: str, status_fn: callable, *args) -> None:
|
||||||
|
status_data: ComponentStatus = status_fn(*args)
|
||||||
|
code: int = status_data.get("status").value.code
|
||||||
|
status: str = status_data.get("status").value.txt
|
||||||
|
repo: str = status_data.get("repo")
|
||||||
|
instance_count: int = status_data.get("instances")
|
||||||
|
|
||||||
|
count_txt: str = ""
|
||||||
|
if instance_count > 0 and code == 1:
|
||||||
|
count_txt = f": {instance_count}"
|
||||||
|
|
||||||
|
setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt))
|
||||||
|
setattr(self, f"{name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT}")
|
||||||
|
|
||||||
|
def _format_by_code(self, code: int, status: str, count: str) -> str:
|
||||||
|
if code == 1:
|
||||||
|
return f"{COLOR_GREEN}{status}{count}{RESET_FORMAT}"
|
||||||
|
elif code == 2:
|
||||||
|
return f"{COLOR_RED}{status}{count}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
return f"{COLOR_YELLOW}{status}{count}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
self.fetch_status()
|
||||||
|
|
||||||
|
header = " [ Main Menu ] "
|
||||||
|
footer1 = "KIAUH v6.0.0"
|
||||||
|
footer2 = f"Changelog: {COLOR_MAGENTA}https://git.io/JnmlX{RESET_FORMAT}"
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
pad1 = 32
|
||||||
|
pad2 = 26
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) [Log-Upload] | Klipper: {self.kl_status:<{pad1}} |
|
||||||
|
| | Repo: {self.kl_repo:<{pad1}} |
|
||||||
|
| 1) [Install] |------------------------------------|
|
||||||
|
| 2) [Update] | Moonraker: {self.mr_status:<{pad1}} |
|
||||||
|
| 3) [Remove] | Repo: {self.mr_repo:<{pad1}} |
|
||||||
|
| 4) [Advanced] |------------------------------------|
|
||||||
|
| 5) [Backup] | Mainsail: {self.ms_status:<{pad2}} |
|
||||||
|
| | Fluidd: {self.fl_status:<{pad2}} |
|
||||||
|
| S) [Settings] | Client-Config: {self.cc_status:<{pad2}} |
|
||||||
|
| | |
|
||||||
|
| Community: | KlipperScreen: {self.ks_status:<{pad2}} |
|
||||||
|
| E) [Extensions] | Mobileraker: {self.mb_status:<{pad2}} |
|
||||||
|
| | Crowsnest: {self.cn_status:<{pad2}} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {COLOR_CYAN}{footer1:^16}{RESET_FORMAT} | {footer2:^43} |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def log_upload_menu(self, **kwargs):
|
||||||
|
LogUploadMenu().run()
|
||||||
|
|
||||||
|
def install_menu(self, **kwargs):
|
||||||
|
InstallMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def update_menu(self, **kwargs):
|
||||||
|
UpdateMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def remove_menu(self, **kwargs):
|
||||||
|
RemoveMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def advanced_menu(self, **kwargs):
|
||||||
|
AdvancedMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def backup_menu(self, **kwargs):
|
||||||
|
BackupMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def settings_menu(self, **kwargs):
|
||||||
|
SettingsMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def extension_menu(self, **kwargs):
|
||||||
|
ExtensionsMenu(previous_menu=self.__class__).run()
|
||||||
95
kiauh/core/menus/remove_menu.py
Normal file
95
kiauh/core/menus/remove_menu.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.crowsnest.crowsnest import remove_crowsnest
|
||||||
|
from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu
|
||||||
|
from components.klipperscreen.klipperscreen import remove_klipperscreen
|
||||||
|
from components.mobileraker.mobileraker import remove_mobileraker
|
||||||
|
from components.moonraker.menus.moonraker_remove_menu import (
|
||||||
|
MoonrakerRemoveMenu,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from components.webui_client.menus.client_remove_menu import ClientRemoveMenu
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import COLOR_RED, RESET_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class RemoveMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self):
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.remove_klipper, menu=True),
|
||||||
|
"2": Option(method=self.remove_moonraker, menu=True),
|
||||||
|
"3": Option(method=self.remove_mainsail, menu=True),
|
||||||
|
"4": Option(method=self.remove_fluidd, menu=True),
|
||||||
|
"5": Option(method=self.remove_klipperscreen, menu=True),
|
||||||
|
"6": Option(method=self.remove_mobileraker, menu=True),
|
||||||
|
"7": Option(method=self.remove_crowsnest, menu=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Remove Menu ] "
|
||||||
|
color = COLOR_RED
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| INFO: Configurations and/or any backups will be kept! |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Firmware & API: | Touchscreen GUI: |
|
||||||
|
| 1) [Klipper] | 5) [KlipperScreen] |
|
||||||
|
| 2) [Moonraker] | |
|
||||||
|
| | Android / iOS: |
|
||||||
|
| Klipper Webinterface: | 6) [Mobileraker] |
|
||||||
|
| 3) [Mainsail] | |
|
||||||
|
| 4) [Fluidd] | Webcam Streamer: |
|
||||||
|
| | 7) [Crowsnest] |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def remove_klipper(self, **kwargs):
|
||||||
|
KlipperRemoveMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def remove_moonraker(self, **kwargs):
|
||||||
|
MoonrakerRemoveMenu(previous_menu=self.__class__).run()
|
||||||
|
|
||||||
|
def remove_mainsail(self, **kwargs):
|
||||||
|
ClientRemoveMenu(previous_menu=self.__class__, client=MainsailData()).run()
|
||||||
|
|
||||||
|
def remove_fluidd(self, **kwargs):
|
||||||
|
ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run()
|
||||||
|
|
||||||
|
def remove_klipperscreen(self, **kwargs):
|
||||||
|
remove_klipperscreen()
|
||||||
|
|
||||||
|
def remove_mobileraker(self, **kwargs):
|
||||||
|
remove_mobileraker()
|
||||||
|
|
||||||
|
def remove_crowsnest(self, **kwargs):
|
||||||
|
remove_crowsnest()
|
||||||
229
kiauh/core/menus/settings_menu.py
Normal file
229
kiauh/core/menus/settings_menu.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 shutil
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple, Type
|
||||||
|
|
||||||
|
from components.klipper import KLIPPER_DIR
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.moonraker import MOONRAKER_DIR
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_GREEN, RESET_FORMAT
|
||||||
|
from utils.git_utils import git_clone_wrapper
|
||||||
|
from utils.input_utils import get_confirm, get_string_input
|
||||||
|
from utils.logger import DialogType, Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class SettingsMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.klipper_repo = None
|
||||||
|
self.moonraker_repo = None
|
||||||
|
self.mainsail_unstable = None
|
||||||
|
self.fluidd_unstable = None
|
||||||
|
self.auto_backups_enabled = None
|
||||||
|
self._load_settings()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"1": Option(method=self.set_klipper_repo, menu=True),
|
||||||
|
"2": Option(method=self.set_moonraker_repo, menu=True),
|
||||||
|
"3": Option(method=self.toggle_mainsail_release, menu=True),
|
||||||
|
"4": Option(method=self.toggle_fluidd_release, menu=False),
|
||||||
|
"5": Option(method=self.toggle_backup_before_update, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ KIAUH Settings ] "
|
||||||
|
color = COLOR_CYAN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
checked = f"[{COLOR_GREEN}x{RESET_FORMAT}]"
|
||||||
|
unchecked = "[ ]"
|
||||||
|
o1 = checked if self.mainsail_unstable else unchecked
|
||||||
|
o2 = checked if self.fluidd_unstable else unchecked
|
||||||
|
o3 = checked if self.auto_backups_enabled else unchecked
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| Klipper source repository: |
|
||||||
|
| ● {self.klipper_repo:<67} |
|
||||||
|
| |
|
||||||
|
| Moonraker source repository: |
|
||||||
|
| ● {self.moonraker_repo:<67} |
|
||||||
|
| |
|
||||||
|
| Install unstable Webinterface releases: |
|
||||||
|
| {o1} Mainsail |
|
||||||
|
| {o2} Fluidd |
|
||||||
|
| |
|
||||||
|
| Auto-Backup: |
|
||||||
|
| {o3} Automatic backup before update |
|
||||||
|
| |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) Set Klipper source repository |
|
||||||
|
| 2) Set Moonraker source repository |
|
||||||
|
| |
|
||||||
|
| 3) Toggle unstable Mainsail releases |
|
||||||
|
| 4) Toggle unstable Fluidd releases |
|
||||||
|
| |
|
||||||
|
| 5) Toggle automatic backups before updates |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def _load_settings(self):
|
||||||
|
self.kiauh_settings = KiauhSettings()
|
||||||
|
|
||||||
|
self._format_repo_str("klipper")
|
||||||
|
self._format_repo_str("moonraker")
|
||||||
|
|
||||||
|
self.auto_backups_enabled = self.kiauh_settings.get(
|
||||||
|
"kiauh",
|
||||||
|
"backup_before_update",
|
||||||
|
)
|
||||||
|
self.mainsail_unstable = self.kiauh_settings.get(
|
||||||
|
"mainsail",
|
||||||
|
"unstable_releases",
|
||||||
|
)
|
||||||
|
self.fluidd_unstable = self.kiauh_settings.get(
|
||||||
|
"fluidd",
|
||||||
|
"unstable_releases",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _format_repo_str(self, repo_name: str) -> None:
|
||||||
|
repo = self.kiauh_settings.get(repo_name, "repo_url")
|
||||||
|
repo = f"{'/'.join(repo.rsplit('/', 2)[-2:])}"
|
||||||
|
branch = self.kiauh_settings.get(repo_name, "branch")
|
||||||
|
branch = f"({COLOR_CYAN}@ {branch}{RESET_FORMAT})"
|
||||||
|
setattr(self, f"{repo_name}_repo", f"{COLOR_CYAN}{repo}{RESET_FORMAT} {branch}")
|
||||||
|
|
||||||
|
def _gather_input(self) -> Tuple[str, str]:
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.ATTENTION,
|
||||||
|
[
|
||||||
|
"There is no input validation in place! Make sure your"
|
||||||
|
" input is valid and has no typos! For any change to"
|
||||||
|
" take effect, the repository must be cloned again. "
|
||||||
|
"Make sure you don't have any ongoing prints running, "
|
||||||
|
"as the services will be restarted!"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
repo = get_string_input(
|
||||||
|
"Enter new repository URL",
|
||||||
|
allow_special_chars=True,
|
||||||
|
)
|
||||||
|
branch = get_string_input(
|
||||||
|
"Enter new branch name",
|
||||||
|
allow_special_chars=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return repo, branch
|
||||||
|
|
||||||
|
def _set_repo(self, repo_name: str):
|
||||||
|
repo_url, branch = self._gather_input()
|
||||||
|
display_name = repo_name.capitalize()
|
||||||
|
Logger.print_dialog(
|
||||||
|
DialogType.CUSTOM,
|
||||||
|
[
|
||||||
|
f"New {display_name} repository URL:",
|
||||||
|
f"● {repo_url}",
|
||||||
|
f"New {display_name} repository branch:",
|
||||||
|
f"● {branch}",
|
||||||
|
],
|
||||||
|
end="",
|
||||||
|
)
|
||||||
|
|
||||||
|
if get_confirm("Apply changes?", allow_go_back=True):
|
||||||
|
self.kiauh_settings.set(repo_name, "repo_url", repo_url)
|
||||||
|
self.kiauh_settings.set(repo_name, "branch", branch)
|
||||||
|
self.kiauh_settings.save()
|
||||||
|
self._load_settings()
|
||||||
|
Logger.print_ok("Changes saved!")
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
f"Skipping change of {display_name} source repository ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status(f"Switching to {display_name}'s new source repository ...")
|
||||||
|
self._switch_repo(repo_name)
|
||||||
|
Logger.print_ok(f"Switched to {repo_url} at branch {branch}!")
|
||||||
|
|
||||||
|
def _switch_repo(self, name: str) -> None:
|
||||||
|
target_dir: Path
|
||||||
|
if name == "klipper":
|
||||||
|
target_dir = KLIPPER_DIR
|
||||||
|
_type = Klipper
|
||||||
|
elif name == "moonraker":
|
||||||
|
target_dir = MOONRAKER_DIR
|
||||||
|
_type = Moonraker
|
||||||
|
else:
|
||||||
|
Logger.print_error("Invalid repository name!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if target_dir.exists():
|
||||||
|
shutil.rmtree(target_dir)
|
||||||
|
|
||||||
|
im = InstanceManager(_type)
|
||||||
|
im.stop_all_instance()
|
||||||
|
|
||||||
|
repo = self.kiauh_settings.get(name, "repo_url")
|
||||||
|
branch = self.kiauh_settings.get(name, "branch")
|
||||||
|
git_clone_wrapper(repo, target_dir, branch)
|
||||||
|
|
||||||
|
im.start_all_instance()
|
||||||
|
|
||||||
|
def set_klipper_repo(self, **kwargs):
|
||||||
|
self._set_repo("klipper")
|
||||||
|
|
||||||
|
def set_moonraker_repo(self, **kwargs):
|
||||||
|
self._set_repo("moonraker")
|
||||||
|
|
||||||
|
def toggle_mainsail_release(self, **kwargs):
|
||||||
|
self.mainsail_unstable = not self.mainsail_unstable
|
||||||
|
self.kiauh_settings.set(
|
||||||
|
"mainsail",
|
||||||
|
"unstable_releases",
|
||||||
|
self.mainsail_unstable,
|
||||||
|
)
|
||||||
|
self.kiauh_settings.save()
|
||||||
|
|
||||||
|
def toggle_fluidd_release(self, **kwargs):
|
||||||
|
self.fluidd_unstable = not self.fluidd_unstable
|
||||||
|
self.kiauh_settings.set(
|
||||||
|
"fluidd",
|
||||||
|
"unstable_releases",
|
||||||
|
self.fluidd_unstable,
|
||||||
|
)
|
||||||
|
self.kiauh_settings.save()
|
||||||
|
|
||||||
|
def toggle_backup_before_update(self, **kwargs):
|
||||||
|
self.auto_backups_enabled = not self.auto_backups_enabled
|
||||||
|
self.kiauh_settings.set(
|
||||||
|
"kiauh",
|
||||||
|
"backup_before_update",
|
||||||
|
self.auto_backups_enabled,
|
||||||
|
)
|
||||||
|
self.kiauh_settings.save()
|
||||||
186
kiauh/core/menus/update_menu.py
Normal file
186
kiauh/core/menus/update_menu.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest
|
||||||
|
from components.klipper.klipper_setup import update_klipper
|
||||||
|
from components.klipper.klipper_utils import (
|
||||||
|
get_klipper_status,
|
||||||
|
)
|
||||||
|
from components.klipperscreen.klipperscreen import (
|
||||||
|
get_klipperscreen_status,
|
||||||
|
update_klipperscreen,
|
||||||
|
)
|
||||||
|
from components.mobileraker.mobileraker import (
|
||||||
|
get_mobileraker_status,
|
||||||
|
update_mobileraker,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker_setup import update_moonraker
|
||||||
|
from components.moonraker.moonraker_utils import get_moonraker_status
|
||||||
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
|
update_client_config,
|
||||||
|
)
|
||||||
|
from components.webui_client.client_setup import update_client
|
||||||
|
from components.webui_client.client_utils import (
|
||||||
|
get_client_config_status,
|
||||||
|
get_client_status,
|
||||||
|
)
|
||||||
|
from components.webui_client.fluidd_data import FluiddData
|
||||||
|
from components.webui_client.mainsail_data import MainsailData
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from utils.constants import (
|
||||||
|
COLOR_GREEN,
|
||||||
|
COLOR_RED,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
RESET_FORMAT,
|
||||||
|
)
|
||||||
|
from utils.types import ComponentStatus
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class UpdateMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
self.kl_local = self.kl_remote = self.mr_local = self.mr_remote = ""
|
||||||
|
self.ms_local = self.ms_remote = self.fl_local = self.fl_remote = ""
|
||||||
|
self.mc_local = self.mc_remote = self.fc_local = self.fc_remote = ""
|
||||||
|
self.ks_local = self.ks_remote = self.mb_local = self.mb_remote = ""
|
||||||
|
self.cn_local = self.cn_remote = ""
|
||||||
|
|
||||||
|
self.mainsail_data = MainsailData()
|
||||||
|
self.fluidd_data = FluiddData()
|
||||||
|
self._fetch_update_status()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
"0": Option(self.update_all, menu=False),
|
||||||
|
"1": Option(self.update_klipper, menu=False),
|
||||||
|
"2": Option(self.update_moonraker, menu=False),
|
||||||
|
"3": Option(self.update_mainsail, menu=False),
|
||||||
|
"4": Option(self.update_fluidd, menu=False),
|
||||||
|
"5": Option(self.update_mainsail_config, menu=False),
|
||||||
|
"6": Option(self.update_fluidd_config, menu=False),
|
||||||
|
"7": Option(self.update_klipperscreen, menu=False),
|
||||||
|
"8": Option(self.update_mobileraker, menu=False),
|
||||||
|
"9": Option(self.update_crowsnest, menu=False),
|
||||||
|
"10": Option(self.upgrade_system_packages, menu=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
self._fetch_update_status()
|
||||||
|
|
||||||
|
header = " [ Update Menu ] "
|
||||||
|
color = COLOR_GREEN
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 0) Update all | | |
|
||||||
|
| | Current: | Latest: |
|
||||||
|
| Klipper & API: |---------------|---------------|
|
||||||
|
| 1) Klipper | {self.kl_local:<22} | {self.kl_remote:<22} |
|
||||||
|
| 2) Moonraker | {self.mr_local:<22} | {self.mr_remote:<22} |
|
||||||
|
| | | |
|
||||||
|
| Webinterface: |---------------|---------------|
|
||||||
|
| 3) Mainsail | {self.ms_local:<22} | {self.ms_remote:<22} |
|
||||||
|
| 4) Fluidd | {self.fl_local:<22} | {self.fl_remote:<22} |
|
||||||
|
| | | |
|
||||||
|
| Client-Config: |---------------|---------------|
|
||||||
|
| 5) Mainsail-Config | {self.mc_local:<22} | {self.mc_remote:<22} |
|
||||||
|
| 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} |
|
||||||
|
| | | |
|
||||||
|
| Other: |---------------|---------------|
|
||||||
|
| 7) KlipperScreen | {self.ks_local:<22} | {self.ks_remote:<22} |
|
||||||
|
| 8) Mobileraker | {self.mb_local:<22} | {self.mb_remote:<22} |
|
||||||
|
| 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} |
|
||||||
|
| |-------------------------------|
|
||||||
|
| 10) System | |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
def update_all(self, **kwargs):
|
||||||
|
print("update_all")
|
||||||
|
|
||||||
|
def update_klipper(self, **kwargs):
|
||||||
|
update_klipper()
|
||||||
|
|
||||||
|
def update_moonraker(self, **kwargs):
|
||||||
|
update_moonraker()
|
||||||
|
|
||||||
|
def update_mainsail(self, **kwargs):
|
||||||
|
update_client(self.mainsail_data)
|
||||||
|
|
||||||
|
def update_mainsail_config(self, **kwargs):
|
||||||
|
update_client_config(self.mainsail_data)
|
||||||
|
|
||||||
|
def update_fluidd(self, **kwargs):
|
||||||
|
update_client(self.fluidd_data)
|
||||||
|
|
||||||
|
def update_fluidd_config(self, **kwargs):
|
||||||
|
update_client_config(self.fluidd_data)
|
||||||
|
|
||||||
|
def update_klipperscreen(self, **kwargs):
|
||||||
|
update_klipperscreen()
|
||||||
|
|
||||||
|
def update_mobileraker(self, **kwargs):
|
||||||
|
update_mobileraker()
|
||||||
|
|
||||||
|
def update_crowsnest(self, **kwargs):
|
||||||
|
update_crowsnest()
|
||||||
|
|
||||||
|
def upgrade_system_packages(self, **kwargs): ...
|
||||||
|
|
||||||
|
def _fetch_update_status(self):
|
||||||
|
# klipper
|
||||||
|
self._get_update_status("kl", get_klipper_status)
|
||||||
|
# moonraker
|
||||||
|
self._get_update_status("mr", get_moonraker_status)
|
||||||
|
# mainsail
|
||||||
|
self._get_update_status("ms", get_client_status, self.mainsail_data, True)
|
||||||
|
# mainsail-config
|
||||||
|
self._get_update_status("mc", get_client_config_status, self.mainsail_data)
|
||||||
|
# fluidd
|
||||||
|
self._get_update_status("fl", get_client_status, self.fluidd_data, True)
|
||||||
|
# fluidd-config
|
||||||
|
self._get_update_status("fc", get_client_config_status, self.fluidd_data)
|
||||||
|
# klipperscreen
|
||||||
|
self._get_update_status("ks", get_klipperscreen_status)
|
||||||
|
# mobileraker
|
||||||
|
self._get_update_status("mb", get_mobileraker_status)
|
||||||
|
# crowsnest
|
||||||
|
self._get_update_status("cn", get_crowsnest_status)
|
||||||
|
|
||||||
|
def _format_local_status(self, local_version, remote_version) -> str:
|
||||||
|
if local_version == remote_version:
|
||||||
|
return f"{COLOR_GREEN}{local_version}{RESET_FORMAT}"
|
||||||
|
return f"{COLOR_YELLOW}{local_version}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
def _get_update_status(self, name: str, status_fn: callable, *args) -> None:
|
||||||
|
status_data: ComponentStatus = status_fn(*args)
|
||||||
|
local_ver = status_data.get("local")
|
||||||
|
remote_ver = status_data.get("remote")
|
||||||
|
color = COLOR_GREEN if remote_ver != "ERROR" else COLOR_RED
|
||||||
|
setattr(self, f"{name}_local", self._format_local_status(local_ver, remote_ver))
|
||||||
|
setattr(self, f"{name}_remote", f"{color}{remote_ver}{RESET_FORMAT}")
|
||||||
0
kiauh/core/settings/__init__.py
Normal file
0
kiauh/core/settings/__init__.py
Normal file
131
kiauh/core/settings/kiauh_settings.py
Normal file
131
kiauh/core/settings/kiauh_settings.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 #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import textwrap
|
||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
|
from core.config_manager.config_manager import CustomConfigParser
|
||||||
|
from utils.constants import COLOR_RED, RESET_FORMAT
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import kill
|
||||||
|
|
||||||
|
from kiauh import PROJECT_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KiauhSettings:
|
||||||
|
_instance = None
|
||||||
|
_default_cfg = PROJECT_ROOT.joinpath("default_kiauh.cfg")
|
||||||
|
_custom_cfg = PROJECT_ROOT.joinpath("kiauh.cfg")
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs) -> "KiauhSettings":
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super(KiauhSettings, cls).__new__(cls, *args, **kwargs)
|
||||||
|
cls._instance.__initialized = False
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if self.__initialized:
|
||||||
|
return
|
||||||
|
self.__initialized = True
|
||||||
|
self.config = CustomConfigParser()
|
||||||
|
self.settings: Dict[str, Dict[str, Union[str, int, bool]]] = {}
|
||||||
|
self._load_settings()
|
||||||
|
|
||||||
|
def get(self, section: str, option: str) -> Union[str, int, bool]:
|
||||||
|
return self.settings[section][option]
|
||||||
|
|
||||||
|
def set(self, section: str, option: str, value: Union[str, int, bool]) -> None:
|
||||||
|
self.settings[section][option] = value
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
for section, option in self.settings.items():
|
||||||
|
self.config[section] = option
|
||||||
|
with open(self._custom_cfg, "w") as configfile:
|
||||||
|
self.config.write(configfile)
|
||||||
|
self._load_settings()
|
||||||
|
|
||||||
|
def _load_settings(self) -> None:
|
||||||
|
if self._custom_cfg.exists():
|
||||||
|
self.config.read(self._custom_cfg)
|
||||||
|
elif self._default_cfg.exists():
|
||||||
|
self.config.read(self._default_cfg)
|
||||||
|
else:
|
||||||
|
self._kill()
|
||||||
|
self._validate_cfg()
|
||||||
|
self._parse_settings()
|
||||||
|
|
||||||
|
def _validate_cfg(self) -> None:
|
||||||
|
try:
|
||||||
|
self._validate_bool("kiauh", "backup_before_update")
|
||||||
|
|
||||||
|
self._validate_str("klipper", "repo_url")
|
||||||
|
self._validate_str("klipper", "branch")
|
||||||
|
|
||||||
|
self._validate_int("mainsail", "port")
|
||||||
|
self._validate_bool("mainsail", "unstable_releases")
|
||||||
|
|
||||||
|
self._validate_int("fluidd", "port")
|
||||||
|
self._validate_bool("fluidd", "unstable_releases")
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
err = f"Invalid value for option '{self._v_option}' in section '{self._v_section}'"
|
||||||
|
Logger.print_error(err)
|
||||||
|
kill()
|
||||||
|
except configparser.NoSectionError:
|
||||||
|
err = f"Missing section '{self._v_section}' in config file"
|
||||||
|
Logger.print_error(err)
|
||||||
|
kill()
|
||||||
|
except configparser.NoOptionError:
|
||||||
|
err = f"Missing option '{self._v_option}' in section '{self._v_section}'"
|
||||||
|
Logger.print_error(err)
|
||||||
|
kill()
|
||||||
|
|
||||||
|
def _validate_bool(self, section: str, option: str) -> None:
|
||||||
|
self._v_section, self._v_option = (section, option)
|
||||||
|
bool(self.config.getboolean(section, option))
|
||||||
|
|
||||||
|
def _validate_int(self, section: str, option: str) -> None:
|
||||||
|
self._v_section, self._v_option = (section, option)
|
||||||
|
int(self.config.getint(section, option))
|
||||||
|
|
||||||
|
def _validate_str(self, section: str, option: str) -> None:
|
||||||
|
self._v_section, self._v_option = (section, option)
|
||||||
|
v = self.config.get(section, option)
|
||||||
|
if v.isdigit() or v.lower() == "true" or v.lower() == "false":
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def _parse_settings(self):
|
||||||
|
for s in self.config.sections():
|
||||||
|
self.settings[s] = {}
|
||||||
|
for o, v in self.config.items(s):
|
||||||
|
if v.lower() == "true":
|
||||||
|
self.settings[s][o] = True
|
||||||
|
elif v.lower() == "false":
|
||||||
|
self.settings[s][o] = False
|
||||||
|
elif v.isdigit():
|
||||||
|
self.settings[s][o] = int(v)
|
||||||
|
else:
|
||||||
|
self.settings[s][o] = v
|
||||||
|
|
||||||
|
def _kill(self) -> None:
|
||||||
|
l1 = "!!! ERROR !!!"
|
||||||
|
l2 = "No KIAUH configuration file found!"
|
||||||
|
error = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
{COLOR_RED}/=======================================================\\
|
||||||
|
| {l1:^53} |
|
||||||
|
| {l2:^53} |
|
||||||
|
\=======================================================/{RESET_FORMAT}
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(error, end="")
|
||||||
|
kill()
|
||||||
12
kiauh/extensions/__init__.py
Normal file
12
kiauh/extensions/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
EXTENSION_ROOT = Path(__file__).resolve().parents[1].joinpath("extensions")
|
||||||
29
kiauh/extensions/base_extension.py
Normal file
29
kiauh/extensions/base_extension.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 abc import ABC, abstractmethod
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class BaseExtension(ABC):
|
||||||
|
def __init__(self, metadata: Dict[str, str]):
|
||||||
|
self.metadata = metadata
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def update_extension(self, **kwargs) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
163
kiauh/extensions/extensions_menu.py
Normal file
163
kiauh/extensions/extensions_menu.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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 importlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Type
|
||||||
|
|
||||||
|
from core.menus import Option
|
||||||
|
from core.menus.base_menu import BaseMenu
|
||||||
|
from extensions import EXTENSION_ROOT
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from utils.constants import COLOR_CYAN, COLOR_YELLOW, RESET_FORMAT
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class ExtensionsMenu(BaseMenu):
|
||||||
|
def __init__(self, previous_menu: Optional[Type[BaseMenu]] = None):
|
||||||
|
super().__init__()
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
self.extensions: Dict[str, BaseExtension] = self.discover_extensions()
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
from core.menus.main_menu import MainMenu
|
||||||
|
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else MainMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options = {
|
||||||
|
i: Option(
|
||||||
|
self.extension_submenu, menu=True, opt_data=self.extensions.get(i)
|
||||||
|
)
|
||||||
|
for i in self.extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
def discover_extensions(self) -> Dict[str, BaseExtension]:
|
||||||
|
ext_dict = {}
|
||||||
|
|
||||||
|
for ext in EXTENSION_ROOT.iterdir():
|
||||||
|
metadata_json = Path(ext).joinpath("metadata.json")
|
||||||
|
if not metadata_json.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(metadata_json, "r") as m:
|
||||||
|
# read extension metadata from json
|
||||||
|
metadata = json.load(m).get("metadata")
|
||||||
|
module_name = metadata.get("module")
|
||||||
|
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]
|
||||||
|
|
||||||
|
# instantiate the extension with its metadata and add to dict
|
||||||
|
ext_instance: BaseExtension = ext_class(metadata)
|
||||||
|
ext_dict[f"{metadata.get('index')}"] = ext_instance
|
||||||
|
|
||||||
|
except (IOError, json.JSONDecodeError, ImportError) as e:
|
||||||
|
print(f"Failed loading extension {ext}: {e}")
|
||||||
|
|
||||||
|
return dict(sorted(ext_dict.items()))
|
||||||
|
|
||||||
|
def extension_submenu(self, **kwargs):
|
||||||
|
ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run()
|
||||||
|
|
||||||
|
def print_menu(self):
|
||||||
|
header = " [ Extensions Menu ] "
|
||||||
|
color = COLOR_CYAN
|
||||||
|
line1 = f"{COLOR_YELLOW}Available Extensions:{RESET_FORMAT}"
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| {line1:<62} |
|
||||||
|
| |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
print(menu, end="")
|
||||||
|
|
||||||
|
for extension in self.extensions.values():
|
||||||
|
index = extension.metadata.get("index")
|
||||||
|
name = extension.metadata.get("display_name")
|
||||||
|
row = f"{index}) {name}"
|
||||||
|
print(f"| {row:<53} |")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class ExtensionSubmenu(BaseMenu):
|
||||||
|
def __init__(
|
||||||
|
self, extension: BaseExtension, previous_menu: Optional[Type[BaseMenu]] = None
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.extension = extension
|
||||||
|
self.previous_menu = previous_menu
|
||||||
|
|
||||||
|
def set_previous_menu(self, previous_menu: Optional[Type[BaseMenu]]) -> None:
|
||||||
|
self.previous_menu: Type[BaseMenu] = (
|
||||||
|
previous_menu if previous_menu is not None else ExtensionsMenu
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_options(self) -> None:
|
||||||
|
self.options["1"] = Option(self.extension.install_extension, menu=False)
|
||||||
|
if self.extension.metadata.get("updates"):
|
||||||
|
self.options["2"] = Option(self.extension.update_extension, menu=False)
|
||||||
|
self.options["3"] = Option(self.extension.remove_extension, menu=False)
|
||||||
|
else:
|
||||||
|
self.options["2"] = Option(self.extension.remove_extension, menu=False)
|
||||||
|
|
||||||
|
def print_menu(self) -> None:
|
||||||
|
header = f" [ {self.extension.metadata.get('display_name')} ] "
|
||||||
|
color = COLOR_YELLOW
|
||||||
|
count = 62 - len(color) - len(RESET_FORMAT)
|
||||||
|
line_width = 53
|
||||||
|
description: List[str] = self.extension.metadata.get("description", [])
|
||||||
|
description_text = Logger.format_content(
|
||||||
|
description,
|
||||||
|
line_width,
|
||||||
|
border_left="|",
|
||||||
|
border_right="|",
|
||||||
|
)
|
||||||
|
|
||||||
|
menu = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
/=======================================================\\
|
||||||
|
| {color}{header:~^{count}}{RESET_FORMAT} |
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
menu += f"{description_text}\n"
|
||||||
|
menu += textwrap.dedent(
|
||||||
|
"""
|
||||||
|
|-------------------------------------------------------|
|
||||||
|
| 1) Install |
|
||||||
|
"""
|
||||||
|
)[1:]
|
||||||
|
|
||||||
|
if self.extension.metadata.get("updates"):
|
||||||
|
menu += "| 2) Update |\n"
|
||||||
|
menu += "| 3) Remove |\n"
|
||||||
|
else:
|
||||||
|
menu += "| 2) Remove |\n"
|
||||||
|
|
||||||
|
print(menu, end="")
|
||||||
19
kiauh/extensions/gcode_shell_cmd/__init__.py
Normal file
19
kiauh/extensions/gcode_shell_cmd/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
EXT_MODULE_NAME = "gcode_shell_command.py"
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
MODULE_ASSETS = MODULE_PATH.joinpath("assets")
|
||||||
|
KLIPPER_DIR = Path.home().joinpath("klipper")
|
||||||
|
KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras")
|
||||||
|
EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME)
|
||||||
|
EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME)
|
||||||
|
EXAMPLE_CFG_SRC = MODULE_ASSETS.joinpath("shell_command.cfg")
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Run a shell command via gcode
|
||||||
|
#
|
||||||
|
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
|
||||||
|
#
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class ShellCommand:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.name = config.get_name().split()[-1]
|
||||||
|
self.printer = config.get_printer()
|
||||||
|
self.gcode = self.printer.lookup_object("gcode")
|
||||||
|
cmd = config.get("command")
|
||||||
|
cmd = os.path.expanduser(cmd)
|
||||||
|
self.command = shlex.split(cmd)
|
||||||
|
self.timeout = config.getfloat("timeout", 2.0, above=0.0)
|
||||||
|
self.verbose = config.getboolean("verbose", True)
|
||||||
|
self.proc_fd = None
|
||||||
|
self.partial_output = ""
|
||||||
|
self.gcode.register_mux_command(
|
||||||
|
"RUN_SHELL_COMMAND",
|
||||||
|
"CMD",
|
||||||
|
self.name,
|
||||||
|
self.cmd_RUN_SHELL_COMMAND,
|
||||||
|
desc=self.cmd_RUN_SHELL_COMMAND_help,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_output(self, eventime):
|
||||||
|
if self.proc_fd is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = os.read(self.proc_fd, 4096)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
data = self.partial_output + data.decode()
|
||||||
|
if "\n" not in data:
|
||||||
|
self.partial_output = data
|
||||||
|
return
|
||||||
|
elif data[-1] != "\n":
|
||||||
|
split = data.rfind("\n") + 1
|
||||||
|
self.partial_output = data[split:]
|
||||||
|
data = data[:split]
|
||||||
|
else:
|
||||||
|
self.partial_output = ""
|
||||||
|
self.gcode.respond_info(data)
|
||||||
|
|
||||||
|
cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command"
|
||||||
|
|
||||||
|
def cmd_RUN_SHELL_COMMAND(self, params):
|
||||||
|
gcode_params = params.get("PARAMS", "")
|
||||||
|
gcode_params = shlex.split(gcode_params)
|
||||||
|
reactor = self.printer.get_reactor()
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
self.command + gcode_params,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("shell_command: Command {%s} failed" % (self.name))
|
||||||
|
raise self.gcode.error("Error running command {%s}" % (self.name))
|
||||||
|
if self.verbose:
|
||||||
|
self.proc_fd = proc.stdout.fileno()
|
||||||
|
self.gcode.respond_info("Running Command {%s}...:" % (self.name))
|
||||||
|
hdl = reactor.register_fd(self.proc_fd, self._process_output)
|
||||||
|
eventtime = reactor.monotonic()
|
||||||
|
endtime = eventtime + self.timeout
|
||||||
|
complete = False
|
||||||
|
while eventtime < endtime:
|
||||||
|
eventtime = reactor.pause(eventtime + 0.05)
|
||||||
|
if proc.poll() is not None:
|
||||||
|
complete = True
|
||||||
|
break
|
||||||
|
if not complete:
|
||||||
|
proc.terminate()
|
||||||
|
if self.verbose:
|
||||||
|
if self.partial_output:
|
||||||
|
self.gcode.respond_info(self.partial_output)
|
||||||
|
self.partial_output = ""
|
||||||
|
if complete:
|
||||||
|
msg = "Command {%s} finished\n" % (self.name)
|
||||||
|
else:
|
||||||
|
msg = "Command {%s} timed out" % (self.name)
|
||||||
|
self.gcode.respond_info(msg)
|
||||||
|
reactor.unregister_fd(hdl)
|
||||||
|
self.proc_fd = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_prefix(config):
|
||||||
|
return ShellCommand(config)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[gcode_shell_command hello_world]
|
||||||
|
command: echo hello world
|
||||||
|
timeout: 2.
|
||||||
|
verbose: True
|
||||||
|
[gcode_macro HELLO_WORLD]
|
||||||
|
gcode:
|
||||||
|
RUN_SHELL_COMMAND CMD=hello_world
|
||||||
127
kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py
Normal file
127
kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.config_manager.config_manager import ConfigManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from extensions.gcode_shell_cmd import (
|
||||||
|
EXAMPLE_CFG_SRC,
|
||||||
|
EXTENSION_SRC,
|
||||||
|
EXTENSION_TARGET_PATH,
|
||||||
|
KLIPPER_DIR,
|
||||||
|
KLIPPER_EXTRAS,
|
||||||
|
)
|
||||||
|
from utils.fs_utils import check_file_exist
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class GcodeShellCmdExtension(BaseExtension):
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
install_example = get_confirm("Create an example shell command?", False, False)
|
||||||
|
|
||||||
|
klipper_dir_exists = check_file_exist(KLIPPER_DIR)
|
||||||
|
if not klipper_dir_exists:
|
||||||
|
Logger.print_warn(
|
||||||
|
"No Klipper directory found! Unable to install extension."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
extension_installed = check_file_exist(EXTENSION_TARGET_PATH)
|
||||||
|
overwrite = True
|
||||||
|
if extension_installed:
|
||||||
|
overwrite = get_confirm(
|
||||||
|
"Extension seems to be installed already. Overwrite?",
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not overwrite:
|
||||||
|
Logger.print_warn("Installation aborted due to user request.")
|
||||||
|
return
|
||||||
|
|
||||||
|
im = InstanceManager(Klipper)
|
||||||
|
im.stop_all_instance()
|
||||||
|
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Copy extension to '{KLIPPER_EXTRAS}' ...")
|
||||||
|
shutil.copy(EXTENSION_SRC, EXTENSION_TARGET_PATH)
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to install extension: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if install_example:
|
||||||
|
self.install_example_cfg(im.instances)
|
||||||
|
|
||||||
|
im.start_all_instance()
|
||||||
|
|
||||||
|
Logger.print_ok("Installing G-Code Shell Command extension successful!")
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
extension_installed = check_file_exist(EXTENSION_TARGET_PATH)
|
||||||
|
if not extension_installed:
|
||||||
|
Logger.print_info("Extension does not seem to be installed! Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
question = "Do you really want to remove the extension?"
|
||||||
|
if get_confirm(question, True, False):
|
||||||
|
try:
|
||||||
|
Logger.print_status(f"Removing '{EXTENSION_TARGET_PATH}' ...")
|
||||||
|
os.remove(EXTENSION_TARGET_PATH)
|
||||||
|
Logger.print_ok("Extension successfully removed!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to remove extension: {e}")
|
||||||
|
|
||||||
|
Logger.print_warn("PLEASE NOTE:")
|
||||||
|
Logger.print_warn(
|
||||||
|
"Remaining gcode shell command will cause Klipper to throw an error."
|
||||||
|
)
|
||||||
|
Logger.print_warn("Make sure to remove them from the printer.cfg!")
|
||||||
|
|
||||||
|
def install_example_cfg(self, instances: List[Klipper]):
|
||||||
|
cfg_dirs = [instance.cfg_dir for instance in instances]
|
||||||
|
# copy extension to klippy/extras
|
||||||
|
for cfg_dir in cfg_dirs:
|
||||||
|
Logger.print_status(f"Create shell_command.cfg in '{cfg_dir}' ...")
|
||||||
|
if check_file_exist(cfg_dir.joinpath("shell_command.cfg")):
|
||||||
|
Logger.print_info("File already exists! Skipping ...")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
shutil.copy(EXAMPLE_CFG_SRC, cfg_dir)
|
||||||
|
Logger.print_ok("Done!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.warn(f"Unable to create example config: {e}")
|
||||||
|
|
||||||
|
# backup each printer.cfg before modification
|
||||||
|
bm = BackupManager()
|
||||||
|
for instance in instances:
|
||||||
|
bm.backup_file(
|
||||||
|
instance.cfg_file,
|
||||||
|
custom_filename=f"{instance.suffix}.printer.cfg",
|
||||||
|
)
|
||||||
|
|
||||||
|
# add section to printer.cfg if not already defined
|
||||||
|
section = "include shell_command.cfg"
|
||||||
|
cfg_files = [instance.cfg_file for instance in instances]
|
||||||
|
for cfg_file in cfg_files:
|
||||||
|
Logger.print_status(f"Include shell_command.cfg in '{cfg_file}' ...")
|
||||||
|
cm = ConfigManager(cfg_file)
|
||||||
|
if cm.config.has_section(section):
|
||||||
|
Logger.print_info("Section already defined! Skipping ...")
|
||||||
|
continue
|
||||||
|
cm.config.add_section(section)
|
||||||
|
cm.write_config()
|
||||||
|
Logger.print_ok("Done!")
|
||||||
9
kiauh/extensions/gcode_shell_cmd/metadata.json
Normal file
9
kiauh/extensions/gcode_shell_cmd/metadata.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 1,
|
||||||
|
"module": "gcode_shell_cmd_extension",
|
||||||
|
"maintained_by": "dw-0",
|
||||||
|
"display_name": "G-Code Shell Command",
|
||||||
|
"description": ["Run a shell commands from gcode."]
|
||||||
|
}
|
||||||
|
}
|
||||||
18
kiauh/extensions/klipper_backup/__init__.py
Normal file
18
kiauh/extensions/klipper_backup/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet #
|
||||||
|
# https://github.com/Staubgeborener/klipper-backup #
|
||||||
|
# #
|
||||||
|
# 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
|
||||||
|
|
||||||
|
EXT_MODULE_NAME = "klipper_backup_extension.py"
|
||||||
|
MODULE_PATH = Path(__file__).resolve().parent
|
||||||
|
MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf")
|
||||||
|
KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup")
|
||||||
|
KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup")
|
||||||
|
KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup"
|
||||||
191
kiauh/extensions/klipper_backup/klipper_backup_extension.py
Normal file
191
kiauh/extensions/klipper_backup/klipper_backup_extension.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet #
|
||||||
|
# https://github.com/Staubgeborener/klipper-backup #
|
||||||
|
# https://klipperbackup.xyz #
|
||||||
|
# #
|
||||||
|
# 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 os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from extensions.base_extension import BaseExtension
|
||||||
|
from extensions.klipper_backup import (
|
||||||
|
KLIPPERBACKUP_CONFIG_DIR,
|
||||||
|
KLIPPERBACKUP_DIR,
|
||||||
|
KLIPPERBACKUP_REPO_URL,
|
||||||
|
MOONRAKER_CONF,
|
||||||
|
)
|
||||||
|
from utils.fs_utils import check_file_exist
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
class KlipperbackupExtension(BaseExtension):
|
||||||
|
def install_extension(self, **kwargs) -> None:
|
||||||
|
if not KLIPPERBACKUP_DIR.exists():
|
||||||
|
subprocess.run(
|
||||||
|
["git", "clone", str(KLIPPERBACKUP_REPO_URL), str(KLIPPERBACKUP_DIR)]
|
||||||
|
)
|
||||||
|
subprocess.run(["chmod", "+x", str(KLIPPERBACKUP_DIR / "install.sh")])
|
||||||
|
subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh")])
|
||||||
|
|
||||||
|
def update_extension(self, **kwargs) -> None:
|
||||||
|
extension_installed = check_file_exist(KLIPPERBACKUP_DIR)
|
||||||
|
if not extension_installed:
|
||||||
|
Logger.print_info("Extension does not seem to be installed! Skipping ...")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh"), "check_updates"])
|
||||||
|
|
||||||
|
def remove_extension(self, **kwargs) -> None:
|
||||||
|
def is_service_installed(service_name):
|
||||||
|
command = ["systemctl", "status", service_name]
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True)
|
||||||
|
# Doesn't matter whether the service is active or not, what matters is whether it is installed. So let's search for "Loaded:" in stdout
|
||||||
|
if "Loaded:" in result.stdout:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def uninstall_service(service_name):
|
||||||
|
try:
|
||||||
|
subprocess.run(["sudo", "systemctl", "stop", service_name], check=True)
|
||||||
|
subprocess.run(
|
||||||
|
["sudo", "systemctl", "disable", service_name], check=True
|
||||||
|
)
|
||||||
|
subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True)
|
||||||
|
service_path = f"/etc/systemd/system/{service_name}"
|
||||||
|
os.system(f"sudo rm {service_path}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_crontab_entry(entry):
|
||||||
|
try:
|
||||||
|
crontab_content = subprocess.check_output(
|
||||||
|
["crontab", "-l"], stderr=subprocess.DEVNULL, text=True
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
for line in crontab_content.splitlines():
|
||||||
|
if entry in line:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
extension_installed = check_file_exist(KLIPPERBACKUP_DIR)
|
||||||
|
if not extension_installed:
|
||||||
|
Logger.print_info("Extension does not seem to be installed! Skipping ...")
|
||||||
|
return
|
||||||
|
|
||||||
|
def remove_moonraker_entry():
|
||||||
|
original_file_path = MOONRAKER_CONF
|
||||||
|
comparison_file_path = os.path.join(
|
||||||
|
str(KLIPPERBACKUP_DIR), "install-files", "moonraker.conf"
|
||||||
|
)
|
||||||
|
if not os.path.exists(original_file_path) or not os.path.exists(
|
||||||
|
comparison_file_path
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
with open(original_file_path, "r") as original_file, open(
|
||||||
|
comparison_file_path, "r"
|
||||||
|
) as comparison_file:
|
||||||
|
original_content = original_file.read()
|
||||||
|
comparison_content = comparison_file.read()
|
||||||
|
if comparison_content in original_content:
|
||||||
|
modified_content = original_content.replace(
|
||||||
|
comparison_content, ""
|
||||||
|
).strip()
|
||||||
|
modified_content = "\n".join(
|
||||||
|
line for line in modified_content.split("\n") if line.strip()
|
||||||
|
)
|
||||||
|
with open(original_file_path, "w") as original_file:
|
||||||
|
original_file.write(modified_content)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
question = "Do you really want to remove the extension?"
|
||||||
|
if get_confirm(question, True, False):
|
||||||
|
# Remove Klipper-Backup services
|
||||||
|
service_names = [
|
||||||
|
"klipper-backup-on-boot.service",
|
||||||
|
"klipper-backup-filewatch.service",
|
||||||
|
]
|
||||||
|
for service_name in service_names:
|
||||||
|
try:
|
||||||
|
Logger.print_status(
|
||||||
|
f"Check whether the service {service_name} is installed ..."
|
||||||
|
)
|
||||||
|
if is_service_installed(service_name):
|
||||||
|
Logger.print_info(f"Service {service_name} detected.")
|
||||||
|
if uninstall_service(service_name):
|
||||||
|
Logger.print_ok(
|
||||||
|
f"The service {service_name} has been successfully uninstalled."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Logger.print_error(
|
||||||
|
f"Error uninstalling the service {service_name}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
f"The service {service_name} is not installed. Skipping ..."
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
Logger.print_error(f"Unable to remove the service {service_name}")
|
||||||
|
|
||||||
|
# Remove Klipper-Backup cron
|
||||||
|
Logger.print_status("Check for Klipper-Backup cron entry ...")
|
||||||
|
entry_to_check = "/klipper-backup/script.sh"
|
||||||
|
try:
|
||||||
|
if check_crontab_entry(entry_to_check):
|
||||||
|
crontab_content = subprocess.check_output(
|
||||||
|
["crontab", "-l"], text=True
|
||||||
|
)
|
||||||
|
modified_content = "\n".join(
|
||||||
|
line
|
||||||
|
for line in crontab_content.splitlines()
|
||||||
|
if entry_to_check not in line
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["crontab", "-"], input=modified_content, text=True, check=True
|
||||||
|
)
|
||||||
|
Logger.print_ok(
|
||||||
|
"The Klipper-Backup entry has been removed from the crontab."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
"The Klipper-Backup entry is not present in the crontab. Skipping ..."
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
Logger.print_error("Unable to remove the Klipper-Backup cron entry")
|
||||||
|
|
||||||
|
# Remove Moonraker entry
|
||||||
|
Logger.print_status("Check for Klipper-Backup moonraker entry ...")
|
||||||
|
try:
|
||||||
|
if remove_moonraker_entry():
|
||||||
|
Logger.print_ok("Klipper-Backup entry in moonraker.conf removed")
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
"Klipper-Backup entry not found in moonraker.conf. Skipping ..."
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
Logger.print_error(
|
||||||
|
"Unknown error, either the moonraker.conf is not found or the Klipper-Backup entry under ~/klipper-backup/install-files/moonraker.conf. Skipping ..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove Klipper-Backup
|
||||||
|
Logger.print_status(f"Removing '{KLIPPERBACKUP_DIR}' ...")
|
||||||
|
try:
|
||||||
|
shutil.rmtree(KLIPPERBACKUP_DIR)
|
||||||
|
config_backup_exists = check_file_exist(KLIPPERBACKUP_CONFIG_DIR)
|
||||||
|
if config_backup_exists:
|
||||||
|
shutil.rmtree(KLIPPERBACKUP_CONFIG_DIR)
|
||||||
|
Logger.print_ok("Extension Klipper-Backup successfully removed!")
|
||||||
|
except OSError as e:
|
||||||
|
Logger.print_error(f"Unable to remove extension: {e}")
|
||||||
10
kiauh/extensions/klipper_backup/metadata.json
Normal file
10
kiauh/extensions/klipper_backup/metadata.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"index": 3,
|
||||||
|
"module": "klipper_backup_extension",
|
||||||
|
"maintained_by": "Staubgeborener",
|
||||||
|
"display_name": "Klipper-Backup",
|
||||||
|
"description": ["Backup all your Klipper files to GitHub"],
|
||||||
|
"updates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user