commit 7693c29676c0b138ec4f32b90d5c6f82bd2b2e13 Author: Cyril Date: Sun Mar 10 21:45:05 2024 +0100 First commit diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7c7a523 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: guilouz diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35aa348 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.DS_Store +desktop.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f88a65 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Creality Helper Script + + + +## Wiki + +Guide is to use it is available here: [Wiki](https://guilouz.github.io/Creality-K1-Series/) + +
\ No newline at end of file diff --git a/files/boot-display/k1_boot_display.tar.gz b/files/boot-display/k1_boot_display.tar.gz new file mode 100644 index 0000000..208a235 Binary files /dev/null and b/files/boot-display/k1_boot_display.tar.gz differ diff --git a/files/boot-display/k1max_boot_display.tar.gz b/files/boot-display/k1max_boot_display.tar.gz new file mode 100644 index 0000000..892d49d Binary files /dev/null and b/files/boot-display/k1max_boot_display.tar.gz differ diff --git a/files/boot-display/stock_boot_display.tar.gz b/files/boot-display/stock_boot_display.tar.gz new file mode 100644 index 0000000..47bdecd Binary files /dev/null and b/files/boot-display/stock_boot_display.tar.gz differ diff --git a/files/buzzer-support/beep.mp3 b/files/buzzer-support/beep.mp3 new file mode 100644 index 0000000..b67241f Binary files /dev/null and b/files/buzzer-support/beep.mp3 differ diff --git a/files/buzzer-support/buzzer-support.cfg b/files/buzzer-support/buzzer-support.cfg new file mode 100644 index 0000000..5ea0be4 --- /dev/null +++ b/files/buzzer-support/buzzer-support.cfg @@ -0,0 +1,14 @@ +######################################## +# Buzzer Support +######################################## + +[gcode_shell_command beep] +command: aplay /usr/data/helper-script/files/buzzer-support/beep.mp3 +timeout: 2 +verbose: False + +[gcode_macro BEEP] + gcode: + RUN_SHELL_COMMAND CMD=beep + RUN_SHELL_COMMAND CMD=beep + RUN_SHELL_COMMAND CMD=beep diff --git a/files/camera-settings/camera-settings.cfg b/files/camera-settings/camera-settings.cfg new file mode 100644 index 0000000..8ea1821 --- /dev/null +++ b/files/camera-settings/camera-settings.cfg @@ -0,0 +1,115 @@ +######################################## +# Camera Settings Control +######################################## + +[delayed_gcode LOAD_CAM_SETTINGS] +initial_duration: 2 +gcode: + CAM_BRIGHTNESS BRIGHTNESS=0 + CAM_CONTRAST CONTRAST=32 + CAM_SATURATION SATURATION=56 + CAM_HUE HUE=0 + CAM_WHITE_BALANCE_TEMPERATURE_AUTO WHITE_BALANCE_TEMPERATURE_AUTO=1 + CAM_GAMMA GAMMA=80 + CAM_GAIN GAIN=0 + CAM_POWER_LINE_FREQUENCY POWER_LINE_FREQUENCY=1 + CAM_WHITE_BALANCE_TEMPERATURE WHITE_BALANCE_TEMPERATURE=4600 + CAM_SHARPNESS SHARPNESS=3 + CAM_BACKLIGHT_COMPENSATION BACKLIGHT_COMPENSATION=1 + CAM_EXPOSURE_AUTO EXPOSURE_AUTO=3 + CAM_EXPOSURE_AUTO_PRIORITY EXPOSURE_AUTO_PRIORITY=0 + CAM_AUTO_FOCUS FOCUS_AUTO=0 + + +[gcode_shell_command v4l2-ctl] +command: v4l2-ctl +timeout: 5.0 +verbose: True + +[gcode_macro CAM_SETTINGS] +gcode: + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 -l" + +[gcode_macro CAM_BRIGHTNESS] +description: min=-64 / max=64 +gcode: + {% set brightness = params.BRIGHTNESS|default(0) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl brightness="{brightness} + +[gcode_macro CAM_CONTRAST] +description: min=0 / max=64 +gcode: + {% set contrast = params.CONTRAST|default(32) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl contrast="{contrast} + +[gcode_macro CAM_SATURATION] +description: min=0 / max=128 +gcode: + {% set saturation = params.SATURATION|default(56) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl saturation="{saturation} + +[gcode_macro CAM_HUE] +description: min=-40 / max=40 +gcode: + {% set hue = params.HUE|default(0) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl hue="{hue} + +[gcode_macro CAM_WHITE_BALANCE_TEMPERATURE_AUTO] +description: disable=0 / enable=1 +gcode: + {% set white_balance_temperature_auto = params.WHITE_BALANCE_TEMPERATURE_AUTO|default(1) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl white_balance_temperature_auto="{white_balance_temperature_auto} + +[gcode_macro CAM_GAMMA] +description: min=72 / max=500 +gcode: + {% set gamma = params.GAMMA|default(80) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl gamma="{gamma} + +[gcode_macro CAM_GAIN] +description: min=0 / max=100 +gcode: + {% set gain = params.GAIN|default(0) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl gain="{gain} + +[gcode_macro CAM_POWER_LINE_FREQUENCY] +description: min=0 / max=2 +gcode: + {% set power_line_frequency = params.POWER_LINE_FREQUENCY|default(1) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl power_line_frequency="{power_line_frequency} + +[gcode_macro CAM_WHITE_BALANCE_TEMPERATURE] +description: min=2800 / max=6500 +gcode: + {% set white_balance_temperature = params.WHITE_BALANCE_TEMPERATURE|default(4600) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl white_balance_temperature="{white_balance_temperature} + +[gcode_macro CAM_SHARPNESS] +description: min=0 / max=6 +gcode: + {% set sharpness = params.SHARPNESS|default(3) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl sharpness="{sharpness} + +[gcode_macro CAM_BACKLIGHT_COMPENSATION] +description: min=0 / max=2 +gcode: + {% set backlight_compensation = params.BACKLIGHT_COMPENSATION|default(1) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl backlight_compensation="{backlight_compensation} + +[gcode_macro CAM_EXPOSURE_AUTO] +description: manual=1 / auto=3 +gcode: + {% set exposure_auto = params.EXPOSURE_AUTO|default(3) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl exposure_auto="{exposure_auto} + +[gcode_macro CAM_EXPOSURE_AUTO_PRIORITY] +description: disable=0 / enable=1 +gcode: + {% set exposure_auto_priority = params.EXPOSURE_AUTO_PRIORITY|default(0) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl exposure_auto_priority="{exposure_auto_priority} + +[gcode_macro CAM_AUTO_FOCUS] +description: disable=0 / enable=1 +gcode: + {% set focus_auto = params.AUTO_FOCUS|default(0) %} + RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl focus_auto="{focus_auto} diff --git a/files/entware/generic.sh b/files/entware/generic.sh new file mode 100755 index 0000000..7b3b99c --- /dev/null +++ b/files/entware/generic.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +unset LD_LIBRARY_PATH +unset LD_PRELOAD + +LOADER=ld.so.1 +GLIBC=2.27 + +echo -e "Info: Removing old directories..." +rm -rf /opt +rm -rf /usr/data/opt + +echo -e "Info: Creating directory..." +mkdir -p /usr/data/opt + +echo -e "Info: Linking folder..." +ln -nsf /usr/data/opt /opt + +echo -e "Info: Creating subdirectories..." +for folder in bin etc lib/opkg tmp var/lock +do + mkdir -p /usr/data/opt/$folder +done + +echo -e "Info: Downloading opkg package manager..." +chmod 755 /usr/data/helper-script/files/fixes/curl +URL="http://www.openk1.org/static/entware/mipselsf-k3.4/installer" +/usr/data/helper-script/files/fixes/curl -L "$URL/opkg" -o "/opt/bin/opkg" +/usr/data/helper-script/files/fixes/curl -L "$URL/opkg.conf" -o "/opt/etc/opkg.conf" + +echo -e "Info: Applying permissions..." +chmod 755 /opt/bin/opkg +chmod 777 /opt/tmp + +echo -e "Info: Installing basic packages..." +/opt/bin/opkg update +/opt/bin/opkg install entware-opt + +echo -e "Info: Installing SFTP server support..." +/opt/bin/opkg install openssh-sftp-server; ln -s /opt/libexec/sftp-server /usr/libexec/sftp-server + +echo -e "Info: Configuring files..." +for file in passwd group shells shadow gshadow; do + if [ -f /etc/$file ]; then + ln -sf /etc/$file /opt/etc/$file + else + [ -f /opt/etc/$file.1 ] && cp /opt/etc/$file.1 /opt/etc/$file + fi +done + +[ -f /etc/localtime ] && ln -sf /etc/localtime /opt/etc/localtime + +echo -e "Info: Applying changes in system profile..." +echo 'export PATH="/opt/bin:/opt/sbin:$PATH"' > /etc/profile.d/entware.sh + +echo -e "Info: Adding startup script..." +echo '#!/bin/sh\n/opt/etc/init.d/rc.unslung "$1"' > /etc/init.d/S50unslung +chmod 755 /etc/init.d/S50unslung diff --git a/files/fixes/curl b/files/fixes/curl new file mode 100755 index 0000000..00cb09f Binary files /dev/null and b/files/fixes/curl differ diff --git a/files/fixes/gcode.py b/files/fixes/gcode.py new file mode 100644 index 0000000..ebdfa40 --- /dev/null +++ b/files/fixes/gcode.py @@ -0,0 +1,549 @@ +# Parse gcode commands +# +# Copyright (C) 2016-2021 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import os, re, logging, collections, shlex +from extras.tool import reportInformation + +class CommandError(Exception): + pass + +Coord = collections.namedtuple('Coord', ('x', 'y', 'z', 'e')) + +class GCodeCommand: + error = CommandError + def __init__(self, gcode, command, commandline, params, need_ack): + self._command = command + self._commandline = commandline + self._params = params + self._need_ack = need_ack + # Method wrappers + self.respond_info = gcode.respond_info + self.respond_raw = gcode.respond_raw + def get_command(self): + return self._command + def get_commandline(self): + return self._commandline + def get_command_parameters(self): + return self._params + def get_raw_command_parameters(self): + command = self._command + if command.startswith("M117 ") or command.startswith("M118 "): + command = command[:4] + rawparams = self._commandline + urawparams = rawparams.upper() + if not urawparams.startswith(command): + rawparams = rawparams[urawparams.find(command):] + end = rawparams.rfind('*') + if end >= 0: + rawparams = rawparams[:end] + rawparams = rawparams[len(command):] + if rawparams.startswith(' '): + rawparams = rawparams[1:] + return rawparams + def ack(self, msg=None): + if not self._need_ack: + return False + ok_msg = "ok" + if msg: + ok_msg = "ok %s" % (msg,) + self.respond_raw(ok_msg) + self._need_ack = False + return True + # Parameter parsing helpers + class sentinel: pass + def get(self, name, default=sentinel, parser=str, minval=None, maxval=None, + above=None, below=None): + value = self._params.get(name) + if value is None: + if default is self.sentinel: + raise self.error("""{"code":"key251", "msg":"Error on '%s': missing %s", "values":["%s",%s"]}""" + % (self._commandline, name, self._commandline, name)) + return default + try: + value = parser(value) + except: + raise self.error( + """{"code":"key171", "msg": "Unable to parse '%s' as a %s", "values": ["%s", "%s"]}""" % (self._commandline, value, + self._commandline, value) + ) + if minval is not None and value < minval: + raise self.error("""{"code":"key252","msg":"Error on '%s': %s must have minimum of %s","values":["%s","%s","%s"]}""" + % (self._commandline, name, minval, self._commandline, name, minval)) + if maxval is not None and value > maxval: + raise self.error("""{"code":"key253", "msg":"Error on '%s': %s must have maximumof %s", "values":["%s","%s","%s"]}""" + % (self._commandline, name, maxval, self._commandline, name, maxval)) + if above is not None and value <= above: + raise self.error("""{"code":"key254", "msg":"Error on '%s': %s must be above %s", "values":["%s","%s","%s"]}""" + % (self._commandline, name, above, self._commandline, name, above)) + if below is not None and value >= below: + raise self.error("""{"code":"key255", "msg":"Error on '%s': %s must be below %s", "values":["%s","%s","%s"]}""" + % (self._commandline, name, below, self._commandline, name, below)) + return value + def get_int(self, name, default=sentinel, minval=None, maxval=None): + return self.get(name, default, parser=int, minval=minval, maxval=maxval) + def get_float(self, name, default=sentinel, minval=None, maxval=None, + above=None, below=None): + return self.get(name, default, parser=float, minval=minval, + maxval=maxval, above=above, below=below) + +# Parse and dispatch G-Code commands +class GCodeDispatch: + error = CommandError + Coord = Coord + def __init__(self, printer): + self.printer = printer + self.is_fileinput = not not printer.get_start_args().get("debuginput") + printer.register_event_handler("klippy:ready", self._handle_ready) + printer.register_event_handler("klippy:shutdown", self._handle_shutdown) + printer.register_event_handler("klippy:disconnect", + self._handle_disconnect) + # Command handling + self.is_printer_ready = False + self.mutex = printer.get_reactor().mutex() + self.output_callbacks = [] + self.base_gcode_handlers = self.gcode_handlers = {} + self.ready_gcode_handlers = {} + self.mux_commands = {} + self.gcode_help = {} + # Register commands needed before config file is loaded + handlers = ['M110', 'M112', 'M115', + 'RESTART', 'FIRMWARE_RESTART', 'ECHO', 'STATUS', 'HELP'] + for cmd in handlers: + func = getattr(self, 'cmd_' + cmd) + desc = getattr(self, 'cmd_' + cmd + '_help', None) + self.register_command(cmd, func, True, desc) + self.last_temperature_info = "/usr/data/creality/userdata/config/temperature_info.json" + self.exclude_object_info = "/usr/data/creality/userdata/config/exclude_object_info.json" + def is_traditional_gcode(self, cmd): + # A "traditional" g-code command is a letter and followed by a number + try: + cmd = cmd.upper().split()[0] + val = float(cmd[1:]) + return cmd[0].isupper() and cmd[1].isdigit() + except: + return False + def register_command(self, cmd, func, when_not_ready=False, desc=None): + if func is None: + old_cmd = self.ready_gcode_handlers.get(cmd) + if cmd in self.ready_gcode_handlers: + del self.ready_gcode_handlers[cmd] + if cmd in self.base_gcode_handlers: + del self.base_gcode_handlers[cmd] + return old_cmd + if cmd in self.ready_gcode_handlers: + raise self.printer.config_error( + """{"code":"key57", "msg":"gcode command %s already registered", "values": ["%s"]}""" % (cmd, cmd)) + if not self.is_traditional_gcode(cmd): + origfunc = func + func = lambda params: origfunc(self._get_extended_params(params)) + self.ready_gcode_handlers[cmd] = func + if when_not_ready: + self.base_gcode_handlers[cmd] = func + if desc is not None: + self.gcode_help[cmd] = desc + def register_mux_command(self, cmd, key, value, func, desc=None): + prev = self.mux_commands.get(cmd) + if prev is None: + handler = lambda gcmd: self._cmd_mux(cmd, gcmd) + self.register_command(cmd, handler, desc=desc) + self.mux_commands[cmd] = prev = (key, {}) + prev_key, prev_values = prev + if prev_key != key: + raise self.printer.config_error( + """{"code":"key58", "msg":"mux command %s %s %s may have only one key (%s)", "values": ["%s", "%s", "%s", "%s"]}""" % ( + cmd, key, value, prev_key, cmd, key, value, prev_key)) + if value in prev_values: + raise self.printer.config_error( + """{"code":"key59", "msg":"mux command %s %s %s already registered (%s)", "values": ["%s", "%s", "%s", "%s"]}""" % ( + cmd, key, value, prev_values, cmd, key, value, prev_values)) + prev_values[value] = func + def get_command_help(self): + return dict(self.gcode_help) + def register_output_handler(self, cb): + self.output_callbacks.append(cb) + def _handle_shutdown(self): + if not self.is_printer_ready: + return + self.is_printer_ready = False + self.gcode_handlers = self.base_gcode_handlers + self._respond_state("Shutdown") + def _handle_disconnect(self): + self._respond_state("Disconnect") + def _handle_ready(self): + self.is_printer_ready = True + self.gcode_handlers = self.ready_gcode_handlers + self._respond_state("Ready") + # Parse input into commands + args_r = re.compile('([A-Z_]+|[A-Z*/])') + def _process_commands(self, commands, need_ack=True): + for line in commands: + # Ignore comments and leading/trailing spaces + line = origline = line.strip() + cpos = line.find(';') + if cpos >= 0: + line = line[:cpos] + # Break line into parts and determine command + parts = self.args_r.split(line.upper()) + numparts = len(parts) + cmd = "" + if numparts >= 3 and parts[1] != 'N': + cmd = parts[1] + parts[2].strip() + elif numparts >= 5 and parts[1] == 'N': + # Skip line number at start of command + cmd = parts[3] + parts[4].strip() + # Build gcode "params" dictionary + params = { parts[i]: parts[i+1].strip() + for i in range(1, numparts, 2) } + gcmd = GCodeCommand(self, cmd, origline, params, need_ack) + # Invoke handler for command + handler = self.gcode_handlers.get(cmd, self.cmd_default) + try: + handler(gcmd) + except self.error as e: + self._respond_error(str(e)) + self.printer.send_event("gcode:command_error") + if not need_ack: + raise + except: + msg = """{"code":"key60", "msg":"Internal error on command:%s", "values": ["%s"]}""" % (cmd, cmd) + logging.exception(msg) + self.printer.invoke_shutdown(msg) + self._respond_error(msg) + if not need_ack: + raise + gcmd.ack() + if line.startswith("G1") or line.startswith("G0"): + pass + elif line.startswith("M104"): + self.set_temperature("extruder", line) + elif line.startswith("M140"): + self.set_temperature("bed", line) + elif line.startswith("M109"): + self.set_temperature("extruder", line) + elif line.startswith("M190"): + self.set_temperature("bed", line) + elif line.startswith("EXCLUDE_OBJECT_DEFINE") or line.startswith("EXCLUDE_OBJECT NAME"): + self.record_exclude_object_info(line) + def set_temperature(self, key, value): + import json + try: + # configfile = self.printer.lookup_object('configfile') + # print_stats = self.printer.load_object(configfile, 'print_stats') + temp_value = float(value.strip("\n").split("S")[-1]) + # if key == "extruder" and print_stats and print_stats.state == "printing": + # if temp_value >= 240: + # self.run_script_from_command("M107 P1") + # logging.info("Fan Off SET M107 P1") + # elif temp_value >= 170: + # self.run_script_from_command("M106 P1 S255") + # logging.info("Fan On SET M106 P1 S255") + if key == "extruder" and temp_value < 170: + return + if not os.path.exists(self.last_temperature_info): + from subprocess import call + call("touch %s" % self.last_temperature_info, shell=True) + with open(self.last_temperature_info, "r") as f: + ret = f.read() + if len(ret) > 0: + ret = json.loads(ret) + else: + ret = {} + ret[key] = temp_value + with open(self.last_temperature_info, "w") as f: + f.write(json.dumps(ret)) + f.flush() + except Exception as err: + logging.error("set_temperature error: %s" % err) + def record_exclude_object_info(self, line): + import json + try: + if not os.path.exists(self.exclude_object_info): + with open(self.exclude_object_info, "w") as f: + data = {} + data["EXCLUDE_OBJECT_DEFINE"] = [] + data["EXCLUDE_OBJECT"] = [] + f.write(json.dumps(data)) + f.flush() + with open(self.exclude_object_info, "r") as f: + ret = f.read() + if len(ret) > 0: + ret = eval(ret) + else: + ret = {} + if line.startswith("EXCLUDE_OBJECT_DEFINE"): + if line not in ret["EXCLUDE_OBJECT_DEFINE"]: + ret["EXCLUDE_OBJECT_DEFINE"].append(line) + elif line.startswith("EXCLUDE_OBJECT NAME"): + if line not in ret["EXCLUDE_OBJECT"]: + ret["EXCLUDE_OBJECT"].append(line) + with open(self.exclude_object_info, "w") as f: + f.write(json.dumps(ret)) + f.flush() + except Exception as err: + logging.error("record_exclude_object_info error: %s" % err) + def run_script_from_command(self, script): + self._process_commands(script.split('\n'), need_ack=False) + def run_script(self, script): + with self.mutex: + self._process_commands(script.split('\n'), need_ack=False) + def get_mutex(self): + return self.mutex + def create_gcode_command(self, command, commandline, params): + return GCodeCommand(self, command, commandline, params, False) + # Response handling + def respond_raw(self, msg): + for cb in self.output_callbacks: + cb(msg) + def respond_info(self, msg, log=True): + if log: + logging.info(msg) + lines = [l.strip() for l in msg.strip().split('\n')] + self.respond_raw("// " + "\n// ".join(lines)) + def _respond_error(self, msg): + from extras.tool import reportInformation + try: + v_sd = self.printer.lookup_object('virtual_sdcard') + if v_sd.print_id and "key" in msg and re.findall('key(\d+)', msg) and v_sd.cur_print_data: + v_sd.update_print_history_info(only_update_status=True, state="error", error_msg=eval(msg)) + v_sd.print_id = "" + reportInformation("key701", data=v_sd.cur_print_data) + v_sd.cur_print_data = {} + except Exception as err: + logging.error(err) + try: + if "key" in msg and re.findall('key(\d+)', msg): + reportInformation(msg) + except Exception as err: + logging.error(err) + logging.warning(msg) + lines = msg.strip().split('\n') + if len(lines) > 1: + self.respond_info("\n".join(lines), log=False) + self.respond_raw('!! %s' % (lines[0].strip(),)) + if self.is_fileinput: + self.printer.request_exit('error_exit') + def _respond_state(self, state): + self.respond_info("Klipper state: %s" % (state,), log=False) + # Parameter parsing helpers + extended_r = re.compile( + r'^\s*(?:N[0-9]+\s*)?' + r'(?P[a-zA-Z_][a-zA-Z0-9_]+)(?:\s+|$)' + r'(?P[^*;]*?)' + r'\s*(?:[#*;].*)?$') + def _get_extended_params(self, gcmd): + m = self.extended_r.match(gcmd.get_commandline()) + if m is None: + raise self.error("""{"code":"key513", "msg": "Malformed command '%s'", "values": ["%s"]}""" % (gcmd.get_commandline(), gcmd.get_commandline())) + eargs = m.group('args') + try: + eparams = [earg.split('=', 1) for earg in shlex.split(eargs)] + eparams = { k.upper(): v for k, v in eparams } + gcmd._params.clear() + gcmd._params.update(eparams) + return gcmd + except ValueError as e: + raise self.error("""{"code":"key514", "msg": "Malformed command args '%s'", "values": ["%s"]}""" % (gcmd.get_commandline(), str(e))) + # G-Code special command handlers + def cmd_default(self, gcmd): + cmd = gcmd.get_command() + if cmd == 'M105': + # Don't warn about temperature requests when not ready + gcmd.ack("T:0") + return + if cmd == 'M21': + # Don't warn about sd card init when not ready + return + if not self.is_printer_ready: + raise gcmd.error(self.printer.get_state_message()[0]) + return + if not cmd: + cmdline = gcmd.get_commandline() + if cmdline: + logging.debug(cmdline) + return + if cmd.startswith("M117 ") or cmd.startswith("M118 "): + # Handle M117/M118 gcode with numeric and special characters + handler = self.gcode_handlers.get(cmd[:4], None) + if handler is not None: + handler(gcmd) + return + elif cmd in ['M140', 'M104'] and not gcmd.get_float('S', 0.): + # Don't warn about requests to turn off heaters when not present + return + elif cmd == 'M107' or (cmd == 'M106' and ( + not gcmd.get_float('S', 1.) or self.is_fileinput)): + # Don't warn about requests to turn off fan when fan not present + return + gcmd.respond_info("""{"code":"key61, "msg":"Unknown command:%s", "values": ["%s"]}""" % (cmd, cmd)) + def get_muxcmd(self, cmdkey): + if cmdkey in self.mux_commands: + key, values = self.mux_commands[cmdkey] + return values + return None + def _cmd_mux(self, command, gcmd): + key, values = self.mux_commands[command] + if None in values: + key_param = gcmd.get(key, None) + else: + key_param = gcmd.get(key) + if key_param not in values: + raise gcmd.error("""{"code":"key69", "msg": "The value '%s' is not valid for %s", "values": ["%s", "%s"]}""" + % (key_param, key, key_param, key)) + values[key_param](gcmd) + # Low-level G-Code commands that are needed before the config file is loaded + def cmd_M110(self, gcmd): + # Set Current Line Number + pass + def cmd_M112(self, gcmd): + # Emergency Stop + self.printer.invoke_shutdown("""{"code":"key70", "msg": "Shutdown due to M112 command", "values": []}""") + def cmd_M115(self, gcmd): + # Get Firmware Version and Capabilities + software_version = self.printer.get_start_args().get('software_version') + kw = {"FIRMWARE_NAME": "Klipper", "FIRMWARE_VERSION": software_version} + msg = " ".join(["%s:%s" % (k, v) for k, v in kw.items()]) + did_ack = gcmd.ack(msg) + if not did_ack: + gcmd.respond_info(msg) + def request_restart(self, result): + if self.is_printer_ready: + toolhead = self.printer.lookup_object('toolhead') + print_time = toolhead.get_last_move_time() + if result == 'exit': + logging.info("Exiting (print time %.3fs)" % (print_time,)) + self.printer.send_event("gcode:request_restart", print_time) + toolhead.dwell(0.500) + toolhead.wait_moves() + self.printer.request_exit(result) + cmd_RESTART_help = "Reload config file and restart host software" + def cmd_RESTART(self, gcmd): + self.request_restart('restart') + cmd_FIRMWARE_RESTART_help = "Restart firmware, host, and reload config" + def cmd_FIRMWARE_RESTART(self, gcmd): + self.request_restart('firmware_restart') + def cmd_ECHO(self, gcmd): + gcmd.respond_info(gcmd.get_commandline(), log=False) + cmd_STATUS_help = "Report the printer status" + def cmd_STATUS(self, gcmd): + if self.is_printer_ready: + self._respond_state("Ready") + return + msg = self.printer.get_state_message()[0] + msg = msg.rstrip() + "\nKlipper state: Not ready" + raise gcmd.error(msg) + cmd_HELP_help = "Report the list of available extended G-Code commands" + def cmd_HELP(self, gcmd): + cmdhelp = [] + if not self.is_printer_ready: + cmdhelp.append("""{"code":"key72", "msg": "Printer is not ready - not all commands available.\n""") + cmdhelp.append("Available extended commands:") + for cmd in sorted(self.gcode_handlers): + if cmd in self.gcode_help: + cmdhelp.append("%-10s: %s" % (cmd, self.gcode_help[cmd])) + gcmd.respond_info("\n".join(cmdhelp), log=False) + +# Support reading gcode from a pseudo-tty interface +class GCodeIO: + def __init__(self, printer): + self.printer = printer + printer.register_event_handler("klippy:ready", self._handle_ready) + printer.register_event_handler("klippy:shutdown", self._handle_shutdown) + self.gcode = printer.lookup_object('gcode') + self.gcode_mutex = self.gcode.get_mutex() + self.fd = printer.get_start_args().get("gcode_fd") + self.reactor = printer.get_reactor() + self.is_printer_ready = False + self.is_processing_data = False + self.is_fileinput = not not printer.get_start_args().get("debuginput") + self.pipe_is_active = True + self.fd_handle = None + if not self.is_fileinput: + self.gcode.register_output_handler(self._respond_raw) + self.fd_handle = self.reactor.register_fd(self.fd, + self._process_data) + self.partial_input = "" + self.pending_commands = [] + self.bytes_read = 0 + self.input_log = collections.deque([], 50) + def _handle_ready(self): + self.is_printer_ready = True + if self.is_fileinput and self.fd_handle is None: + self.fd_handle = self.reactor.register_fd(self.fd, + self._process_data) + def _dump_debug(self): + out = [] + out.append("Dumping gcode input %d blocks" % (len(self.input_log),)) + for eventtime, data in self.input_log: + out.append("Read %f: %s" % (eventtime, repr(data))) + logging.info("\n".join(out)) + def _handle_shutdown(self): + if not self.is_printer_ready: + return + self.is_printer_ready = False + self._dump_debug() + if self.is_fileinput: + self.printer.request_exit('error_exit') + m112_r = re.compile('^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)') + def _process_data(self, eventtime): + # Read input, separate by newline, and add to pending_commands + try: + data = str(os.read(self.fd, 4096).decode()) + except (os.error, UnicodeDecodeError): + logging.exception("Read g-code") + return + self.input_log.append((eventtime, data)) + self.bytes_read += len(data) + lines = data.split('\n') + lines[0] = self.partial_input + lines[0] + self.partial_input = lines.pop() + pending_commands = self.pending_commands + pending_commands.extend(lines) + self.pipe_is_active = True + # Special handling for debug file input EOF + if not data and self.is_fileinput: + if not self.is_processing_data: + self.reactor.unregister_fd(self.fd_handle) + self.fd_handle = None + self.gcode.request_restart('exit') + pending_commands.append("") + # Handle case where multiple commands pending + if self.is_processing_data or len(pending_commands) > 1: + if len(pending_commands) < 20: + # Check for M112 out-of-order + for line in lines: + if self.m112_r.match(line) is not None: + self.gcode.cmd_M112(None) + if self.is_processing_data: + if len(pending_commands) >= 20: + # Stop reading input + self.reactor.unregister_fd(self.fd_handle) + self.fd_handle = None + return + # Process commands + self.is_processing_data = True + while pending_commands: + self.pending_commands = [] + with self.gcode_mutex: + self.gcode._process_commands(pending_commands) + pending_commands = self.pending_commands + self.is_processing_data = False + if self.fd_handle is None: + self.fd_handle = self.reactor.register_fd(self.fd, + self._process_data) + def _respond_raw(self, msg): + if self.pipe_is_active: + try: + os.write(self.fd, (msg+"\n").encode()) + # if 'key506' not in msg and 'key507' not in msg and 'key3"' not in msg and "key" in msg: + # reportInformation(msg) + except os.error: + logging.exception("Write g-code response") + self.pipe_is_active = False + def stats(self, eventtime): + return False, "gcodein=%d" % (self.bytes_read,) + +def add_early_printer_objects(printer): + printer.add_object('gcode', GCodeDispatch(printer)) + printer.add_object('gcode_io', GCodeIO(printer)) diff --git a/files/fixes/sudo b/files/fixes/sudo new file mode 100755 index 0000000..fa1d094 --- /dev/null +++ b/files/fixes/sudo @@ -0,0 +1,2 @@ +#!/bin/sh +exec $* \ No newline at end of file diff --git a/files/fixes/supervisorctl b/files/fixes/supervisorctl new file mode 100755 index 0000000..75749fd --- /dev/null +++ b/files/fixes/supervisorctl @@ -0,0 +1,131 @@ +#!/bin/sh +# supervisorctl shim - by destinal +# this is a fake supervisorctl that provides just enough information for moonraker to think it's the real thing. +# good enough to list the names of services in moonraker.conf, to say whether they're running or not (with false pids and times) +# and to start and stop them by name, finding and calling the matching init scripts. +# installing: put this in in /usr/bin/supervisorctl and then in moonraker.conf in [machine] section, set "provider: supervisord_cli" + +if [ -t 1 ]; then # colorize only if we're on a terminal + GREEN='\033[32m' + RED='\033[31m' + ENDCOLOR='\033[0m' +fi + +get_services() { + moonraker_pid="$(cat /var/run/moonraker.pid)" + # if moonraker is running, get its config directory from its command line + if [ -f /var/run/moonraker.pid ] && [ -d /proc/"$moonraker_pid" ] ; then + cmdline="$(tr '\0' '\n' < /proc/"$moonraker_pid"/cmdline)" + moonraker_dir="$(echo $cmdline | awk -F'-d ' '{print $2}' | awk '{print $1}')" + moonraker_conf="$moonraker_dir/config/moonraker.conf" + # services="klipper moonraker $(awk '/managed_services:/ {print $2}' $moonraker_conf | sed 's/://')" + # services=`(printf 'klipper\nmoonraker\n'; awk '/managed_services:/ {print $2}' $moonraker_conf | sed 's/://') | sort|uniq` + services=$(ls -1 /etc/init.d/S*|sed 's/.*\/S..//;s/_service$//') + echo $services + else + echo "Error: Invalid or missing PID file /var/run/moonraker.pid" >&2 + exit 1 + fi +} + +get_pid_file() { + service="$1" + [ $service == "klipper" ] && service="klippy" + pid_file="/var/run/$service.pid" + echo $pid_file +} + +is_running() { + service="$1" + pid_file="$(get_pid_file "$service")" + + # Check for PID file + if [ -f "$pid_file" ] && [ -d "/proc/$(cat $pid_file)" ]; then + return 0 # Running + fi + + # Fallback to using pidof in case the service doesn't use pid files + if pidof "$service" &>/dev/null; then + return 0 # Running + fi + return 1 # Not running +} + +print_process_status() { + if is_running "$service"; then + printf "%-33s$GREEN""RUNNING$ENDCOLOR\n" "$service" + else + printf "%-33s$RED""STOPPED$ENDCOLOR\n" "$service" + fi +} + +print_usage() { + echo "supervisorctl shim - provide minimal support for moonraker so CrealityOS moonraker can start/stop without systemd" + echo "Usage: $0 [command] " + echo "commands include status stop start restart" +} + +get_script_path() { + service="$1" + script_path="$(ls -1 /etc/init.d/S[0-9][0-9]${service}_service /etc/init.d/S[0-9][0-9]${service}* 2>/dev/null|head -1)" + echo "$script_path" +} + +stop() { + service="$1" + script_path="$(get_script_path $service)" + # Check if the script exists and stop the service + if [[ -f "$script_path" ]]; then + "$script_path" stop + fi +} + +start() { + service="$1" + script_path="$(get_script_path $service)" + # Check if the script exists and start the service + if [[ -f "$script_path" ]]; then + "$script_path" start + fi +} + +restart() { + service="$1" + script_path="$(get_script_path $service)" + # Check if the script exists and restart the service + if [[ -f "$script_path" ]]; then + "$script_path" restart + fi +} + +main() { + # echo "$0 $@" >> /tmp/supervisorctl.log + action="$1"; shift + case "$action" in + status) + if [ "$#" -lt 1 ]; then # just status, no arguments + for service in $(get_services); do + print_process_status $service + done + else + for service in "$@"; do # loop through the arguments provided + print_process_status $service + done + fi + ;; + start) + start "$1" + ;; + stop) + stop "$1" + ;; + restart) + restart "$1" + ;; + *) + print_usage + exit 1 + esac +} + +main "$@" diff --git a/files/fixes/systemctl b/files/fixes/systemctl new file mode 100755 index 0000000..7b8e794 --- /dev/null +++ b/files/fixes/systemctl @@ -0,0 +1,7 @@ +#!/bin/sh + +if [ "$1" == "reboot" ]; then + /sbin/reboot +elif [ "$1" == "poweroff" ]; then + /sbin/poweroff +fi \ No newline at end of file diff --git a/files/fluidd-logos/config.json b/files/fluidd-logos/config.json new file mode 100644 index 0000000..ef9a16b --- /dev/null +++ b/files/fluidd-logos/config.json @@ -0,0 +1,167 @@ +{ + "blacklist": [ + "fluidd.xyz", + "fluidd.net" + ], + "endpoints": [ + ], + "hosted": false, + "themePresets": [ + { + "name": "Fluidd", + "color": "#2196F3", + "isDark": true, + "logo": { + "src": "logo_fluidd.svg" + } + }, + { + "name": "Annex", + "color": "#96CC4A", + "isDark": true, + "logo": { + "src": "logo_annex.svg" + } + }, + { + "name": "BTT", + "color": "#475A91", + "isDark": true, + "logo": { + "src": "logo_btt.svg" + } + }, + { + "name": "Creality V1", + "color": "#2196F3", + "isDark": true, + "logo": { + "src": "logo_creality_v1.svg" + } + }, + { + "name": "Creality V2", + "color": "#2196F3", + "isDark": true, + "logo": { + "src": "logo_creality_v2.svg" + } + }, + { + "name": "EVA", + "color": "#76FB00", + "isDark": true, + "logo": { + "src": "logo_eva.svg", + "dark": "#232323", + "light": "#ffffff" + } + }, + { + "name": "HevORT", + "color": "#dfff3e", + "isDark": true, + "logo": { + "src": "logo_hevort.svg" + } + }, + { + "name": "Kingroon", + "color": "#DA7A2C", + "isDark": true, + "logo": { + "src": "logo_kingroon.svg" + } + }, + { + "name": "Klipper", + "color": "#B12F36", + "isDark": true, + "logo": { + "src": "logo_klipper.svg" + } + }, + { + "name": "LDO", + "color": "#326799", + "isDark": true, + "logo": { + "src": "logo_ldo.svg" + } + }, + { + "name": "Peopoly", + "color": "#007CC2", + "isDark": true, + "logo": { + "src": "logo_peopoly.svg" + } + }, + { + "name": "Prusa", + "color": "#E05D2D", + "isDark": false, + "logo": { + "src": "logo_prusa.svg" + } + }, + { + "name": "Qidi Tech", + "color": "#5B7AEA", + "isDark": true, + "logo": { + "src": "logo_qidi.svg" + } + }, + { + "name": "RatRig", + "color": "#76FB00", + "isDark": true, + "logo": { + "src": "logo_ratrig.svg", + "dark": "#232323", + "light": "#ffffff" + } + }, + { + "name": "Siboor", + "color": "#32E0DF", + "isDark": true, + "logo": { + "src": "logo_siboor.svg" + } + }, + { + "name": "Voron", + "color": "#FF2300", + "isDark": true, + "logo": { + "src": "logo_voron.svg" + } + }, + { + "name": "VzBot", + "color": "#FF2300", + "isDark": true, + "logo": { + "src": "logo_vzbot.svg" + } + }, + { + "name": "ZeroG", + "color": "#e34234", + "isDark": true, + "logo": { + "src": "logo_zerog.svg" + } + }, + { + "name": "SnakeOil", + "color": "#4bc3ca", + "isDark": true, + "logo": { + "src": "logo_snakeoil.svg" + } + } + ] +} diff --git a/files/fluidd-logos/logo_creality_v1.svg b/files/fluidd-logos/logo_creality_v1.svg new file mode 100644 index 0000000..74ce505 --- /dev/null +++ b/files/fluidd-logos/logo_creality_v1.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/files/fluidd-logos/logo_creality_v2.svg b/files/fluidd-logos/logo_creality_v2.svg new file mode 100644 index 0000000..40b3783 --- /dev/null +++ b/files/fluidd-logos/logo_creality_v2.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/files/gcode-shell-command/gcode_shell_command.py b/files/gcode-shell-command/gcode_shell_command.py new file mode 100644 index 0000000..bb38ae5 --- /dev/null +++ b/files/gcode-shell-command/gcode_shell_command.py @@ -0,0 +1,87 @@ +# Run a shell command via gcode +# +# Copyright (C) 2019 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import os +import shlex +import subprocess +import logging + +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., above=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 + .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) diff --git a/files/git-backup/S52Git-Backup b/files/git-backup/S52Git-Backup new file mode 100755 index 0000000..8825b0f --- /dev/null +++ b/files/git-backup/S52Git-Backup @@ -0,0 +1,25 @@ +#!/bin/sh + +case "$1" in + start) + echo "Starting Git Backup..." + /usr/data/helper-script/files/git-backup/git-backup.sh -b "$BRANCH" -t "$IFS" -g origin & > /dev/null + ;; + stop) + echo "Stopping Git Backup..." + pkill Git-Backup + pkill inotifywait + ;; + restart) + echo "Restarting Git Backup..." + pkill Git-Backup + pkill inotifywait + sleep 1 + /usr/data/helper-script/files/git-backup/git-backup.sh -b "$BRANCH" -t "$IFS" -g origin & > /dev/null + ;; + *) + Usage: $0 {start|stop|restart} + exit 1 + ;; +esac +exit 0 diff --git a/files/git-backup/git-backup.cfg b/files/git-backup/git-backup.cfg new file mode 100644 index 0000000..51ccd57 --- /dev/null +++ b/files/git-backup/git-backup.cfg @@ -0,0 +1,35 @@ +######################################## +# Git Backup +######################################## + +[gcode_shell_command Backup_Stop] +command: sh /usr/data/helper-script/files/git-backup/git-backup.sh -s +timeout: 600.0 +verbose: true + + +[gcode_shell_command Backup_Pause] +command: sh /usr/data/helper-script/files/git-backup/git-backup.sh -p +timeout: 600.0 +verbose: true + + +[gcode_shell_command Backup_Resume] +command: sh /usr/data/helper-script/files/git-backup/git-backup.sh -s +timeout: 600.0 +verbose: true + + +[gcode_macro GIT_BACKUP_STOP] +gcode: + RUN_SHELL_COMMAND CMD=Backup_Stop + + +[gcode_macro GIT_BACKUP_PAUSE] +gcode: + RUN_SHELL_COMMAND CMD=Backup_Pause + + +[gcode_macro GIT_BACKUP_RESUME] +gcode: + RUN_SHELL_COMMAND CMD=Backup_Resume diff --git a/files/git-backup/git-backup.sh b/files/git-backup/git-backup.sh new file mode 100755 index 0000000..4efa5ab --- /dev/null +++ b/files/git-backup/git-backup.sh @@ -0,0 +1,310 @@ +#!/bin/sh +# +# This program is based off of gitwatch @ https://github.com/gitwatch/gitwatch.git +# Copyright (C) 2013-2018 Patrick Lehner +# with modifications and contributions by: +# - Matthew McGowan +# - Dominik D. Geyer +# - Phil Thompson +# - Dave Musicant +# +# Edited to work on busybox ash shell, specifically the Creality K1 & K1Max +############################################################################# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################# +# +# Idea and original code taken from http://stackoverflow.com/a/965274 +# original work by Lester Buck +# (but heavily modified by now) +# +# Requires the command 'inotifywait' to be available, which is part of +# the inotify-tools (See https://github.com/rvoicilas/inotify-tools ), +# and (obviously) git. +# Will check the availability of both commands using the `which` command +# and will abort if either command (or `which`) is not found. +# + +white=`echo -en "\033[m"` +yellow=`echo -en "\033[1;33m"` +green=`echo -en "\033[01;32m"` + +INSTALL=0 +PAUSE=0 +RESUME=0 +STOP=0 +REMOTE="" +BRANCH="" +TARGET="" +EVENTS="${EVENTS:-close_write,move,move_self,delete,create,modify}" +SLEEP_TIME=5 +DATE_FMT="+%d-%m-%Y (%H:%M:%S)" +COMMITMSG="Auto-commit on %d by Git Backup" +SKIP_IF_MERGING=0 + +# Function to print script help +shelp() { + echo "Usage: $(basename "$0") [-i] [-p] [-r] [-s] -b branch -t target -g remote" + echo "Options:" + echo " -i Install" + echo " -p Pause" + echo " -r Resume" + echo " -s Stop" + echo " -b branch Specify branch for git push" + echo " -t target Specify target directory or file to watch" + echo " -g remote Specify remote for git push" +} + +# Parse command-line arguments +while getopts "iprsb:t:g:hn" option; do + case "${option}" in + i) INSTALL=1 ;; + p) PAUSE=1 ;; + r) RESUME=1 ;; + s) STOP=1 ;; + b) BRANCH="${OPTARG}" ;; + t) TARGET="${OPTARG}" ;; + g) REMOTE="${OPTARG}" ;; + h) + shelp + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + shelp + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + shelp + exit 1 + ;; + *) + shelp + exit 0 + ;; + esac +done + +# Check if more than one flag is used +if [ "$((INSTALL + PAUSE + RESUME + STOP))" -gt 1 ]; then + echo "Error: Only one flag is allowed at a time." + shelp + exit 1 +fi + +# Pause, Resume, Stop flags +if [ "$PAUSE" = 1 ]; then + echo "Info: Pausing automatic backups until the next reboot or manually restarted..." + /etc/init.d/S52Git-Backup stop + exit 0 +elif [ "$STOP" = 1 ]; then + echo "Info: Stopping automatic backups until manually restarted..." + mv /etc/init.d/S52Git-Backup /etc/init.d/disabled.S52Git-Backup + exit 0 +elif [ "$RESUME" = 1 ]; then + echo "Info: Resuming automatic backups..." + mv /etc/init.d/disabled.S52Git-Backup /etc/init.d/S52Git-Backup + exit 0 +elif [ "$INSTALL" = 1 ]; then + # Install required packages using opkg + if [ -f /opt/bin/opkg ]; then + /opt/bin/opkg update + /opt/bin/opkg install inotifywait procps-ng-pkill + else + echo "Error: opkg package manager not found. Please install Entware." + exit 1 + fi + + # Prompt user for configuration + echo "${white}" + read -p " Please enter your ${green}GitHub username${white} and press Enter: ${yellow}" USER_NAME + echo "${white}" + read -p " Please enter your ${green}GitHub repository name${white} and press Enter: ${yellow}" REPO_NAME + echo "${white}" + read -p " Please enter your ${green}GitHub personal access token${white} and press Enter: ${yellow}" GITHUB_TOKEN + echo "${white}" + + # Prompt user to select folders to be watched + IFS=/usr/data/printer_data/config + + # Connect config directory to github + cd "$IFS" || exit + git init + git remote add origin "https://$USER_NAME:$GITHUB_TOKEN@github.com/$USER_NAME/$REPO_NAME.git" + git checkout -b "$BRANCH" + git add . + git commit -m "Initial Backup" + git push -u origin "$BRANCH" + + # Write configuration to .env file + echo "IFS=$IFS" > "$IFS/.env" + echo "GITHUB_TOKEN=$GITHUB_TOKEN" >> "$IFS/.env" + echo "REMOTE=$REPO_NAME" >> "$IFS/.env" + echo "BRANCH=$BRANCH" >> "$IFS/.env" + echo "USER=$USER_NAME" >> "$IFS/.env" + + # Create .gitignore file to protect .env variables + echo ".env" > "$IFS/.gitignore" + + # Insert .env to S52gitwatch.sh and move to init.d + cp -f /usr/data/helper-script/files/git-backup/S52Git-Backup /etc/init.d/S52Git-Backup + sed -i "2i source $IFS/.env" /etc/init.d/S52Git-Backup + chmod +x /etc/init.d/S52Git-Backup + /etc/init.d/S52Git-Backup start + + exit 0 +fi + +# print all arguments to stderr +stderr() { + echo "$@" >&2 +} + +# clean up at end of program, killing the remaining sleep process if it still exists +cleanup() { + if [ -n "$SLEEP_PID" ] && kill -0 "$SLEEP_PID" 2>/dev/null; then + kill "$SLEEP_PID" 2>/dev/null + fi + exit 0 +} + +# Tests for the availability of a command +is_command() { + command -v "$1" >/dev/null 2>&1 +} + +# Test whether or not current git directory has ongoing merge +is_merging () { + [ -f "$(git rev-parse --git-dir)"/MERGE_HEAD ] +} + +shift $((OPTIND - 1)) # Shift the input arguments, so that the input file (last arg) is $1 in the code below + +GIT="git" +RL="readlink" +INW="inotifywait" + +# Check availability of selected binaries and die if not met +for cmd in "$GIT" "$INW"; do + is_command "$cmd" || { + stderr "Error: Required command '$cmd' not found." + exit 2 + } +done + +SLEEP_PID="" # pid of timeout subprocess + +trap "cleanup" EXIT # make sure the timeout is killed when exiting script + +# Expand the path to the target to absolute path +if [ "$(uname)" != "Darwin" ]; then + IN=$($RL -f "$TARGET") +else + if is_command "greadlink"; then + IN=$(greadlink -f "$TARGET") + else + IN=$($RL -f "$TARGET") + if [ $? -eq 1 ]; then + echo "Info: Seems like your readlink doesn't support '-f'. Running without. Please 'brew install coreutils'." + IN=$($RL "$TARGET") + fi + fi +fi + +if [ -d "$TARGET" ]; then # if the target is a directory + + TARGETDIR=$(echo "$IN" | sed -e "s/\/*$//") # dir to CD into before using git commands: trim trailing slash, if any + + # construct inotifywait-commandline + if [ "$(uname)" != "Darwin" ]; then + INW_ARGS="-qmr -e $EVENTS $TARGETDIR" + fi + GIT_ADD="git add -A ." # add "." (CWD) recursively to index + GIT_COMMIT_ARGS="-a" # add -a switch to "commit" call just to be sure + +else + stderr "Error: The target is neither a regular file nor a directory." + exit 3 +fi + +# CD into the right dir +cd "$TARGETDIR" || { + stderr "Error: Can't change directory to '${TARGETDIR}'." + exit 5 +} + +if [ -n "$REMOTE" ]; then # are we pushing to a remote? + if [ -z "$BRANCH" ]; then # Do we have a branch set to push to ? + PUSH_CMD="$GIT push $REMOTE" # Branch not set, push to remote without a branch + else + # check if we are on a detached HEAD + if HEADREF=$($GIT symbolic-ref HEAD 2> /dev/null); then # HEAD is not detached + PUSH_CMD="$GIT push $REMOTE ${HEADREF#refs/heads/}:$BRANCH" + else # HEAD is detached + PUSH_CMD="$GIT push $REMOTE $BRANCH" + fi + fi +else + PUSH_CMD="" # if no remote is selected, make sure the push command is empty +fi + +# main program loop: wait for changes and commit them +# whenever inotifywait reports a change, we spawn a timer (sleep process) that gives the writing +# process some time (in case there are a lot of changes or w/e); if there is already a timer +# running when we receive an event, we kill it and start a new one; thus we only commit if there +# have been no changes reported during a whole timeout period +# Custom timeout function +# main program loop: wait for changes and commit them +# Custom timeout function +timeout() { + sleep "5" & + timeout_pid=$! + trap "kill $timeout_pid 2>/dev/null" EXIT + wait $timeout_pid 2>/dev/null +} + +while true; do + # Start inotifywait to monitor changes + eval "$INW $INW_ARGS" | while read -r line; do + # Check if there were any changes reported during the timeout period + if [ -n "$line" ]; then + # Process changes + if [ -n "$DATE_FMT" ]; then + COMMITMSG=$(echo "$COMMITMSG" | awk -v date="$(date "$DATE_FMT")" '{gsub(/%d/, date)}1') # splice the formatted date-time into the commit message + fi + + cd "$TARGETDIR" || { + stderr "Error: Can't change directory to '${TARGETDIR}'." + exit 6 + } + STATUS=$($GIT status -s) + if [ -n "$STATUS" ]; then # only commit if status shows tracked changes. + if [ "$SKIP_IF_MERGING" -eq 1 ] && is_merging; then + echo "Skipping commit - repo is merging" + continue + fi + + $GIT_ADD # add file(s) to index + $GIT commit $GIT_COMMIT_ARGS -m "$COMMITMSG" # construct commit message and commit + + if [ -n "$PUSH_CMD" ]; then + echo "Push command is $PUSH_CMD" + eval "$PUSH_CMD" + pkill 'inotifywait' + timeout + fi + fi + fi + done +done diff --git a/files/guppy-screen/guppy-update.sh b/files/guppy-screen/guppy-update.sh new file mode 100644 index 0000000..ee9eb42 --- /dev/null +++ b/files/guppy-screen/guppy-update.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +GUPPY_DIR="/usr/data/guppyscreen" +CURL="/usr/data/helper-script/files/fixes/curl" +VERSION_FILE="$GUPPY_DIR/.version" +CUSTOM_UPGRADE_SCRIPT="$GUPPY_DIR/custom_upgrade.sh" + +if [ -f "$VERSION_FILE" ]; then + CURRENT_VERSION=$(jq -r '.version' "$VERSION_FILE") + THEME=$(jq -r '.theme' "$VERSION_FILE") + ASSET_NAME=$(jq '.asset_name' "$VERSION_FILE") +fi + +"$CURL" -s https://api.github.com/repos/ballaswag/guppyscreen/releases -o /tmp/guppy-releases.json +latest_version=$(jq -r '.[0].tag_name' /tmp/guppy-releases.json) +if [ "$(printf '%s\n' "$CURRENT_VERSION" "$latest_version" | sort -V | head -n1)" = "$latest_version" ]; then + echo "Guppy Screen $CURRENT_VERSION is already up to date!" + rm -f /tmp/guppy-releases.json + exit 0 +else + asset_url=$(jq -r ".[0].assets[] | select(.name == $ASSET_NAME).browser_download_url" /tmp/guppy-releases.json) + echo "Downloading latest version $latest_version from $asset_url" + "$CURL" -L "$asset_url" -o /usr/data/guppyscreen.tar.gz +fi + +tar -xvf /usr/data/guppyscreen.tar.gz -C "$GUPPY_DIR/.." + +if [ -f "$CUSTOM_UPGRADE_SCRIPT" ]; then + echo "Running custom_upgrade.sh for release $latest_version..." + "$CUSTOM_UPGRADE_SCRIPT" +fi + +echo "Guppy Screen have been updated to version $latest_version!" + +if grep -Fqs "ID=buildroot" /etc/os-release +then + [ -f /etc/init.d/S99guppyscreen ] && /etc/init.d/S99guppyscreen stop &> /dev/null + killall -q guppyscreen + /etc/init.d/S99guppyscreen restart &> /dev/null + rm -f /usr/data/guppyscreen.tar.gz + rm -f /tmp/guppy-releases.json +fi + +exit 0 diff --git a/files/guppy-screen/guppy_update.cfg b/files/guppy-screen/guppy_update.cfg new file mode 100644 index 0000000..45a4d99 --- /dev/null +++ b/files/guppy-screen/guppy_update.cfg @@ -0,0 +1,37 @@ +######################################## +# Guppy Screen Update +######################################## + +[gcode_shell_command guppy_update] +command: sh /usr/data/helper-script/files/guppy-screen/guppy-update.sh +timeout: 600.0 +verbose: True + + +[gcode_macro GUPPY_UPDATE] +description: Check for Guppy Screen Updates +gcode: + {% if printer.idle_timeout.state == "Printing" %} + RESPOND TYPE=error MSG="It's not possible to update Guppy Screen while printing!" + {% else %} + RUN_SHELL_COMMAND CMD=guppy_update + {% endif %} + + +[gcode_macro INPUT_SHAPER_CALIBRATION] +description: Measure X and Y Axis Resonances and Save values +gcode: + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30 + {% endif %} + {% if printer.toolhead.homed_axes != "xyz" %} + RESPOND TYPE=command MSG="Homing..." + G28 + {% endif %} + RESPOND TYPE=command MSG="Measuring X and Y Resonances..." + SHAPER_CALIBRATE + M400 + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50 + {% endif %} + CXSAVE_CONFIG \ No newline at end of file diff --git a/files/improved-shapers/calibrate_shaper_config.py b/files/improved-shapers/calibrate_shaper_config.py new file mode 100755 index 0000000..bf26e36 --- /dev/null +++ b/files/improved-shapers/calibrate_shaper_config.py @@ -0,0 +1,39 @@ +class CalibrateShaperConfig: + def __init__(self, config): + self.printer = config.get_printer(); + + shaper_type = config.get('shaper_type', 'mzv') + self.shaper_type_x = config.get('shaper_type_x' , shaper_type) + self.shaper_freq_x = config.getfloat('shaper_freq_x', 0., minval=0.) + + self.shaper_type_y = config.get('shaper_type_y' , shaper_type) + self.shaper_freq_y = config.getfloat('shaper_freq_y', 0., minval=0.) + + # Register commands + gcode = config.get_printer().lookup_object('gcode') + gcode.register_command("SAVE_INPUT_SHAPER", self.cmd_save_input_shaper) + + def get_status(self, eventtime): + return {} + + def cmd_save_input_shaper(self, gcmd): + self.shaper_freq_x = gcmd.get_float('SHAPER_FREQ_X', + self.shaper_freq_x, minval=0.) + self.shaper_type_x = gcmd.get('SHAPER_TYPE_X', self.shaper_type_x) + + self.shaper_freq_y = gcmd.get_float('SHAPER_FREQ_Y', + self.shaper_freq_y, minval=0.) + self.shaper_type_y = gcmd.get('SHAPER_TYPE_Y', self.shaper_type_y) + + configfile = self.printer.lookup_object('configfile') + + configfile.set('input_shaper', 'shaper_type_x', self.shaper_type_x) + configfile.set('input_shaper', 'shaper_freq_x', + '%.1f' % (self.shaper_freq_x,)) + + configfile.set('input_shaper', 'shaper_type_y', self.shaper_type_y) + configfile.set('input_shaper', 'shaper_freq_y', + '%.1f' % (self.shaper_freq_y,)) + +def load_config(config): + return CalibrateShaperConfig(config) diff --git a/files/improved-shapers/ft2font.cpython-38-mipsel-linux-gnu.so b/files/improved-shapers/ft2font.cpython-38-mipsel-linux-gnu.so new file mode 100755 index 0000000..12d211c Binary files /dev/null and b/files/improved-shapers/ft2font.cpython-38-mipsel-linux-gnu.so differ diff --git a/files/improved-shapers/improved-shapers.cfg b/files/improved-shapers/improved-shapers.cfg new file mode 100644 index 0000000..ebba306 --- /dev/null +++ b/files/improved-shapers/improved-shapers.cfg @@ -0,0 +1,118 @@ +######################################## +# Improved Shapers Configurations +######################################## + +[respond] + +[calibrate_shaper_config] + + +[gcode_shell_command resonance_graph] +command: /usr/data/printer_data/config/Helper-Script/improved-shapers/scripts/calibrate_shaper.py +timeout: 600.0 +verbose: True + + +[gcode_shell_command belts_graph] +command: /usr/data/printer_data/config/Helper-Script/improved-shapers/scripts/graph_belts.py +timeout: 600.0 +verbose: True + + +[gcode_macro INPUT_SHAPER_CALIBRATION] +description: Measure X and Y Axis Resonances and Save values +gcode: + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30 + {% endif %} + {% if printer.toolhead.homed_axes != "xyz" %} + RESPOND TYPE=command MSG="Homing..." + G28 + {% endif %} + RESPOND TYPE=command MSG="Measuring X and Y Resonances..." + SHAPER_CALIBRATE + M400 + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50 + {% endif %} + CXSAVE_CONFIG + + +[gcode_macro TEST_RESONANCES_GRAPHS] +description: Test X and Y Axis Resonances and Generate Graphs +gcode: + {% set x_png = params.X_PNG|default("/usr/data/printer_data/config/Helper-Script/improved-shapers/resonances_x.png") %} + {% set y_png = params.Y_PNG|default("/usr/data/printer_data/config/Helper-Script/improved-shapers/resonances_y.png") %} + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30 + {% endif %} + {% if printer.toolhead.homed_axes != "xyz" %} + RESPOND TYPE=command MSG="Homing..." + G28 + {% endif %} + RESPOND TYPE=command MSG="Testing X Resonances..." + TEST_RESONANCES AXIS=X NAME=x + M400 + RESPOND TYPE=command MSG="Generating X Graphs... This may take some time." + RUN_SHELL_COMMAND CMD=resonance_graph PARAMS="/tmp/resonances_x_x.csv -o {x_png}" + RESPOND TYPE=command MSG="X Graph (resonances_x.png) is available in /Helper-Script/improved-shapers folder." + RESPOND TYPE=command MSG="Testing Y Resonances..." + TEST_RESONANCES AXIS=Y NAME=y + M400 + RESPOND TYPE=command MSG="Generating Y Graphs... This may take some time." + RUN_SHELL_COMMAND CMD=resonance_graph PARAMS="/tmp/resonances_y_y.csv -o {y_png}" + RESPOND TYPE=command MSG="Y Graph (resonances_y.png) is available in /Helper-Script/improved-shapers folder." + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50 + {% endif %} + + +[gcode_macro BELTS_SHAPER_CALIBRATION] +description: Perform a custom half-axis test to analyze and compare the frequency profiles of individual belts on CoreXY printers +gcode: + {% set min_freq = params.FREQ_START|default(5)|float %} + {% set max_freq = params.FREQ_END|default(133.33)|float %} + {% set hz_per_sec = params.HZ_PER_SEC|default(1)|float %} + {% set png_width = params.PNG_WIDTH|default(8)|float %} + {% set png_height = params.PNG_HEIGHT|default(4.8)|float %} + {% set png_out_path = params.PNG_OUT_PATH|default("/usr/data/printer_data/config/Helper-Script/improved-shapers/belts_calibration.png") %} + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30 + {% endif %} + {% if printer.toolhead.homed_axes != "xyz" %} + RESPOND TYPE=command MSG="Homing..." + G28 + {% endif %} + TEST_RESONANCES AXIS=1,1 OUTPUT=raw_data NAME=b FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec} + M400 + TEST_RESONANCES AXIS=1,-1 OUTPUT=raw_data NAME=a FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec} + M400 + RESPOND TYPE=command MSG="Generating belts comparative frequency profile..." + RESPOND TYPE=command MSG="This may take some time (3-5min)." + RUN_SHELL_COMMAND CMD=belts_graph PARAMS="-w {png_width} -l {png_height} -n -o {png_out_path} -k /usr/share/klipper /tmp/raw_data_axis=1.000,-1.000_a.csv /tmp/raw_data_axis=1.000,1.000_b.csv" + RESPOND TYPE=command MSG="Graph (belts_calibration.png) is available in /Helper-Script/improved-shapers folder." + {% if printer["configfile"].config["temperature_fan mcu_fan"] %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50 + {% endif %} + + +[gcode_macro EXCITATE_AXIS_AT_FREQ] +description: Maintain a specified excitation frequency for a period of time to diagnose and locate a vibration source +gcode: + {% set frequency = params.FREQUENCY|default(25)|int %} + {% set time = params.TIME|default(10)|int %} + {% set axis = params.AXIS|default("x")|string|lower %} + {% if axis not in ["x", "y", "a", "b"] %} + { action_raise_error("AXIS selection is invalid. Should be either x, y, a or b!") } + {% endif %} + {% if axis == "a" %} + {% set axis = "1,-1" %} + {% elif axis == "b" %} + {% set axis = "1,1" %} + {% endif %} + {% if printer.toolhead.homed_axes != "xyz" %} + RESPOND TYPE=command MSG="Homing..." + G28 + {% endif %} + TEST_RESONANCES OUTPUT=raw_data AXIS={axis} FREQ_START={frequency-1} FREQ_END={frequency+1} HZ_PER_SEC={1/(time/3)} + M400 diff --git a/files/improved-shapers/scripts/calibrate_shaper.py b/files/improved-shapers/scripts/calibrate_shaper.py new file mode 100755 index 0000000..fcff3cb --- /dev/null +++ b/files/improved-shapers/scripts/calibrate_shaper.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +###!/usr/data/rootfs/usr/bin/python3 +# Shaper auto-calibration script +# +# Copyright (C) 2020 Dmitry Butyugin +# Copyright (C) 2020 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +from __future__ import print_function +import importlib, optparse, os, sys, pathlib +from textwrap import wrap +import numpy as np, matplotlib +import shaper_calibrate +import json + +MAX_TITLE_LENGTH=65 + +def parse_log(logname): + with open(logname) as f: + for header in f: + if not header.startswith('#'): + break + if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + # Raw accelerometer data + return np.loadtxt(logname, comments='#', delimiter=',') + # Parse power spectral density data + data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',') + calibration_data = shaper_calibrate.CalibrationData( + freq_bins=data[:,0], psd_sum=data[:,4], + psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3]) + calibration_data.set_numpy(np) + # If input shapers are present in the CSV file, the frequency + # response is already normalized to input frequencies + if 'mzv' not in header: + calibration_data.normalize_to_frequencies() + return calibration_data + +###################################################################### +# Shaper calibration +###################################################################### + +# Find the best shaper parameters +def calibrate_shaper(datas, csv_output, max_smoothing): + helper = shaper_calibrate.ShaperCalibrate(printer=None) + if isinstance(datas[0], shaper_calibrate.CalibrationData): + calibration_data = datas[0] + for data in datas[1:]: + calibration_data.add_data(data) + else: + # Process accelerometer data + calibration_data = helper.process_accelerometer_data(datas[0]) + for data in datas[1:]: + calibration_data.add_data(helper.process_accelerometer_data(data)) + calibration_data.normalize_to_frequencies() + shaper, all_shapers, resp = helper.find_best_shaper( + calibration_data, max_smoothing, print) + if csv_output is not None: + helper.save_calibration_data( + csv_output, calibration_data, all_shapers) + return shaper.name, all_shapers, calibration_data, resp + +###################################################################### +# Plot frequency response and suggested input shapers +###################################################################### + +def plot_freq_response(lognames, calibration_data, shapers, + selected_shaper, max_freq): + freqs = calibration_data.freq_bins + psd = calibration_data.psd_sum[freqs <= max_freq] + px = calibration_data.psd_x[freqs <= max_freq] + py = calibration_data.psd_y[freqs <= max_freq] + pz = calibration_data.psd_z[freqs <= max_freq] + freqs = freqs[freqs <= max_freq] + + fontP = matplotlib.font_manager.FontProperties() + fontP.set_size('small') + + fig, ax = matplotlib.pyplot.subplots() + ax.set_xlabel('Frequency, Hz') + ax.set_xlim([0, max_freq]) + ax.set_ylabel('Power spectral density') + + ax.plot(freqs, psd, label='X+Y+Z', color='purple') + ax.plot(freqs, px, label='X', color='red') + ax.plot(freqs, py, label='Y', color='green') + ax.plot(freqs, pz, label='Z', color='blue') + + title = "Frequency response and shapers (%s)" % (', '.join(lognames)) + ax.set_title("\n".join(wrap(title, MAX_TITLE_LENGTH))) + ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5)) + ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0)) + ax.grid(which='major', color='grey') + ax.grid(which='minor', color='lightgrey') + + ax2 = ax.twinx() + ax2.set_ylabel('Shaper vibration reduction (ratio)') + best_shaper_vals = None + for shaper in shapers: + label = "%s (%.1f Hz, vibr=%.1f%%, sm~=%.2f, accel<=%.f)" % ( + shaper.name.upper(), shaper.freq, + shaper.vibrs * 100., shaper.smoothing, + round(shaper.max_accel / 100.) * 100.) + linestyle = 'dotted' + if shaper.name == selected_shaper: + linestyle = 'dashdot' + best_shaper_vals = shaper.vals + ax2.plot(freqs, shaper.vals, label=label, linestyle=linestyle) + ax.plot(freqs, psd * best_shaper_vals, + label='After\nshaper', color='cyan') + # A hack to add a human-readable shaper recommendation to legend + ax2.plot([], [], ' ', + label="Recommended shaper: %s" % (selected_shaper.upper())) + + ax.legend(loc='upper left', prop=fontP) + ax2.legend(loc='upper right', prop=fontP) + + fig.tight_layout() + return fig + +###################################################################### +# Startup +###################################################################### + +def setup_matplotlib(output_to_file): + global matplotlib + if output_to_file: + matplotlib.rcParams.update({'figure.autolayout': True}) + matplotlib.use('Agg') + import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager + import matplotlib.ticker + +def main(): + # Parse command-line arguments + usage = "%prog [options] " + opts = optparse.OptionParser(usage) + opts.add_option("-o", "--output", type="string", dest="output", + default=None, help="filename of output graph") + opts.add_option("-c", "--csv", type="string", dest="csv", + default=None, help="filename of output csv file") + opts.add_option("-f", "--max_freq", type="float", default=200., + help="maximum frequency to graph") + opts.add_option("-s", "--max_smoothing", type="float", default=None, + help="maximum shaper smoothing to allow") + opts.add_option("-w", "--width", type="float", dest="width", + default=8.3, help="width (inches) of the graph(s)") + opts.add_option("-l", "--height", type="float", dest="height", + default=11.6, help="height (inches) of the graph(s)") + + options, args = opts.parse_args() + if len(args) < 1: + opts.error("Incorrect number of arguments") + if options.max_smoothing is not None and options.max_smoothing < 0.05: + opts.error("Too small max_smoothing specified (must be at least 0.05)") + + # Parse data + datas = [parse_log(fn) for fn in args] + + # Calibrate shaper and generate outputs + selected_shaper, shapers, calibration_data, resp = calibrate_shaper( + datas, options.csv, options.max_smoothing) + + resp['logfile'] = args[0] + + if not options.csv or options.output: + # Draw graph + setup_matplotlib(options.output is not None) + + fig = plot_freq_response(args, calibration_data, shapers, + selected_shaper, options.max_freq) + + # Show graph + if options.output is None: + matplotlib.pyplot.show() + else: + pathlib.Path(options.output).unlink(missing_ok=True) + fig.set_size_inches(options.width, options.height) + fig.savefig(options.output) + resp['png'] = options.output + + print(json.dumps(resp)) + print + + +if __name__ == '__main__': + main() diff --git a/files/improved-shapers/scripts/graph_belts.py b/files/improved-shapers/scripts/graph_belts.py new file mode 100755 index 0000000..a160504 --- /dev/null +++ b/files/improved-shapers/scripts/graph_belts.py @@ -0,0 +1,573 @@ +#!/usr/bin/env python3 + +################################################# +######## CoreXY BELTS CALIBRATION SCRIPT ######## +################################################# +# Written by Frix_x#0161 # + +# Be sure to make this script executable using SSH: type 'chmod +x ./graph_belts.py' when in the folder! + +##################################################################### +################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ +##################################################################### + +import optparse, matplotlib, sys, importlib, os, pathlib +from collections import namedtuple +import numpy as np +import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager +import matplotlib.ticker, matplotlib.gridspec, matplotlib.colors +import matplotlib.patches +import locale +import time +import glob +import shaper_calibrate +from datetime import datetime + +matplotlib.use('Agg') + + +ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # For paired peaks names + +PEAKS_DETECTION_THRESHOLD = 0.20 +CURVE_SIMILARITY_SIGMOID_K = 0.6 +DC_GRAIN_OF_SALT_FACTOR = 0.75 +DC_THRESHOLD_METRIC = 1.5e9 +DC_MAX_UNPAIRED_PEAKS_ALLOWED = 4 + +# Define the SignalData namedtuple +SignalData = namedtuple('CalibrationData', ['freqs', 'psd', 'peaks', 'paired_peaks', 'unpaired_peaks']) + +KLIPPAIN_COLORS = { + "purple": "#70088C", + "orange": "#FF8D32", + "dark_purple": "#150140", + "dark_orange": "#F24130", + "red_pink": "#F2055C" +} + + +# Set the best locale for time and date formating (generation of the titles) +try: + locale.setlocale(locale.LC_TIME, locale.getdefaultlocale()) +except locale.Error: + locale.setlocale(locale.LC_TIME, 'C') + +# Override the built-in print function to avoid problem in Klipper due to locale settings +original_print = print +def print_with_c_locale(*args, **kwargs): + original_locale = locale.setlocale(locale.LC_ALL, None) + locale.setlocale(locale.LC_ALL, 'C') + original_print(*args, **kwargs) + locale.setlocale(locale.LC_ALL, original_locale) +print = print_with_c_locale + + +def is_file_open(filepath): + for proc in os.listdir('/proc'): + if proc.isdigit(): + for fd in glob.glob(f'/proc/{proc}/fd/*'): + try: + if os.path.samefile(fd, filepath): + return True + except FileNotFoundError: + # Klipper has already released the CSV file + pass + except PermissionError: + # Unable to check for this particular process due to permissions + pass + return False + + +###################################################################### +# Computation of the PSD graph +###################################################################### + +# Calculate estimated "power spectral density" using existing Klipper tools +def calc_freq_response(data): + helper = shaper_calibrate.ShaperCalibrate(printer=None) + return helper.process_accelerometer_data(data) + + +# Calculate or estimate a "similarity" factor between two PSD curves and scale it to a percentage. This is +# used here to quantify how close the two belts path behavior and responses are close together. +def compute_curve_similarity_factor(signal1, signal2): + freqs1 = signal1.freqs + psd1 = signal1.psd + freqs2 = signal2.freqs + psd2 = signal2.psd + + # Interpolate PSDs to match the same frequency bins and do a cross-correlation + psd2_interp = np.interp(freqs1, freqs2, psd2) + cross_corr = np.correlate(psd1, psd2_interp, mode='full') + + # Find the peak of the cross-correlation and compute a similarity normalized by the energy of the signals + peak_value = np.max(cross_corr) + similarity = peak_value / (np.sqrt(np.sum(psd1**2) * np.sum(psd2_interp**2))) + + # Apply sigmoid scaling to get better numbers and get a final percentage value + scaled_similarity = sigmoid_scale(-np.log(1 - similarity), CURVE_SIMILARITY_SIGMOID_K) + + return scaled_similarity + + +# This find all the peaks in a curve by looking at when the derivative term goes from positive to negative +# Then only the peaks found above a threshold are kept to avoid capturing peaks in the low amplitude noise of a signal +def detect_peaks(psd, freqs, window_size=5, vicinity=3): + # Smooth the curve using a moving average to avoid catching peaks everywhere in noisy signals + kernel = np.ones(window_size) / window_size + smoothed_psd = np.convolve(psd, kernel, mode='valid') + mean_pad = [np.mean(psd[:window_size])] * (window_size // 2) + smoothed_psd = np.concatenate((mean_pad, smoothed_psd)) + + # Find peaks on the smoothed curve + smoothed_peaks = np.where((smoothed_psd[:-2] < smoothed_psd[1:-1]) & (smoothed_psd[1:-1] > smoothed_psd[2:]))[0] + 1 + detection_threshold = PEAKS_DETECTION_THRESHOLD * psd.max() + smoothed_peaks = smoothed_peaks[smoothed_psd[smoothed_peaks] > detection_threshold] + + # Refine peak positions on the original curve + refined_peaks = [] + for peak in smoothed_peaks: + local_max = peak + np.argmax(psd[max(0, peak-vicinity):min(len(psd), peak+vicinity+1)]) - vicinity + refined_peaks.append(local_max) + + return np.array(refined_peaks), freqs[refined_peaks] + + +# This function create pairs of peaks that are close in frequency on two curves (that are known +# to be resonances points and must be similar on both belts on a CoreXY kinematic) +def pair_peaks(peaks1, freqs1, psd1, peaks2, freqs2, psd2): + # Compute a dynamic detection threshold to filter and pair peaks efficiently + # even if the signal is very noisy (this get clipped to a maximum of 10Hz diff) + distances = [] + for p1 in peaks1: + for p2 in peaks2: + distances.append(abs(freqs1[p1] - freqs2[p2])) + distances = np.array(distances) + + median_distance = np.median(distances) + iqr = np.percentile(distances, 75) - np.percentile(distances, 25) + + threshold = median_distance + 1.5 * iqr + threshold = min(threshold, 10) + + # Pair the peaks using the dynamic thresold + paired_peaks = [] + unpaired_peaks1 = list(peaks1) + unpaired_peaks2 = list(peaks2) + + while unpaired_peaks1 and unpaired_peaks2: + min_distance = threshold + 1 + pair = None + + for p1 in unpaired_peaks1: + for p2 in unpaired_peaks2: + distance = abs(freqs1[p1] - freqs2[p2]) + if distance < min_distance: + min_distance = distance + pair = (p1, p2) + + if pair is None: # No more pairs below the threshold + break + + p1, p2 = pair + paired_peaks.append(((p1, freqs1[p1], psd1[p1]), (p2, freqs2[p2], psd2[p2]))) + unpaired_peaks1.remove(p1) + unpaired_peaks2.remove(p2) + + return paired_peaks, unpaired_peaks1, unpaired_peaks2 + + +###################################################################### +# Computation of a basic signal spectrogram +###################################################################### + +def compute_spectrogram(data): + import scipy + + N = data.shape[0] + Fs = N / (data[-1, 0] - data[0, 0]) + # Round up to a power of 2 for faster FFT + M = 1 << int(.5 * Fs - 1).bit_length() + window = np.kaiser(M, 6.) + + def _specgram(x): + x_detrended = x - np.mean(x) # Detrending by subtracting the mean value + return scipy.signal.spectrogram( + x_detrended, fs=Fs, window=window, nperseg=M, noverlap=M//2, + detrend='constant', scaling='density', mode='psd') + + d = {'x': data[:, 1], 'y': data[:, 2], 'z': data[:, 3]} + f, t, pdata = _specgram(d['x']) + for axis in 'yz': + pdata += _specgram(d[axis])[2] + return pdata, t, f + + +###################################################################### +# Computation of the differential spectrogram +###################################################################### + +# Interpolate source_data (2D) to match target_x and target_y in order to +# get similar time and frequency dimensions for the differential spectrogram +def interpolate_2d(target_x, target_y, source_x, source_y, source_data): + import scipy + + # Create a grid of points in the source and target space + source_points = np.array([(x, y) for y in source_y for x in source_x]) + target_points = np.array([(x, y) for y in target_y for x in target_x]) + + # Flatten the source data to match the flattened source points + source_values = source_data.flatten() + + # Interpolate and reshape the interpolated data to match the target grid shape and replace NaN with zeros + interpolated_data = scipy.interpolate.griddata(source_points, source_values, target_points, method='nearest') + interpolated_data = interpolated_data.reshape((len(target_y), len(target_x))) + interpolated_data = np.nan_to_num(interpolated_data) + + return interpolated_data + + +# Main logic function to combine two similar spectrogram - ie. from both belts paths - by substracting signals in order to create +# a new composite spectrogram. This result of a divergent but mostly centered new spectrogram (center will be white) with some colored zones +# highlighting differences in the belts paths. The summative spectrogram is used for the MHI calculation. +def combined_spectrogram(data1, data2): + pdata1, bins1, t1 = compute_spectrogram(data1) + pdata2, bins2, t2 = compute_spectrogram(data2) + + # Interpolate the spectrograms + pdata2_interpolated = interpolate_2d(bins1, t1, bins2, t2, pdata2) + + # Cobine them in two form: a summed diff for the MHI computation and a diverging diff for the spectrogram colors + combined_sum = np.abs(pdata1 - pdata2_interpolated) + combined_divergent = pdata1 - pdata2_interpolated + + return combined_sum, combined_divergent, bins1, t1 + + +# Compute a composite and highly subjective value indicating the "mechanical health of the printer (0 to 100%)" that represent the +# likelihood of mechanical issues on the printer. It is based on the differential spectrogram sum of gradient, salted with a bit +# of the estimated similarity cross-correlation from compute_curve_similarity_factor() and with a bit of the number of unpaired peaks. +# This result in a percentage value quantifying the machine behavior around the main resonances that give an hint if only touching belt tension +# will give good graphs or if there is a chance of mechanical issues in the background (above 50% should be considered as probably problematic) +def compute_mhi(combined_data, similarity_coefficient, num_unpaired_peaks): + # filtered_data = combined_data[combined_data > 100] + filtered_data = np.abs(combined_data) + + # First compute a "total variability metric" based on the sum of the gradient that sum the magnitude of will emphasize regions of the + # spectrogram where there are rapid changes in magnitude (like the edges of resonance peaks). + total_variability_metric = np.sum(np.abs(np.gradient(filtered_data))) + # Scale the metric to a percentage using the threshold (found empirically on a large number of user data shared to me) + base_percentage = (np.log1p(total_variability_metric) / np.log1p(DC_THRESHOLD_METRIC)) * 100 + + # Adjust the percentage based on the similarity_coefficient to add a grain of salt + adjusted_percentage = base_percentage * (1 - DC_GRAIN_OF_SALT_FACTOR * (similarity_coefficient / 100)) + + # Adjust the percentage again based on the number of unpaired peaks to add a second grain of salt + peak_confidence = num_unpaired_peaks / DC_MAX_UNPAIRED_PEAKS_ALLOWED + final_percentage = (1 - peak_confidence) * adjusted_percentage + peak_confidence * 100 + + # Ensure the result lies between 0 and 100 by clipping the computed value + final_percentage = np.clip(final_percentage, 0, 100) + + return final_percentage, mhi_lut(final_percentage) + + +# LUT to transform the MHI into a textual value easy to understand for the users of the script +def mhi_lut(mhi): + if 0 <= mhi <= 30: + return "Excellent mechanical health" + elif 30 < mhi <= 45: + return "Good mechanical health" + elif 45 < mhi <= 55: + return "Acceptable mechanical health" + elif 55 < mhi <= 70: + return "Potential signs of a mechanical issue" + elif 70 < mhi <= 85: + return "Likely a mechanical issue" + elif 85 < mhi <= 100: + return "Mechanical issue detected" + + +###################################################################### +# Graphing +###################################################################### + +def plot_compare_frequency(ax, lognames, signal1, signal2, max_freq): + # Get the belt name for the legend to avoid putting the full file name + signal1_belt = (lognames[0].split('/')[-1]).split('_')[-1][0] + signal2_belt = (lognames[1].split('/')[-1]).split('_')[-1][0] + + if signal1_belt == 'A' and signal2_belt == 'B': + signal1_belt += " (axis 1,-1)" + signal2_belt += " (axis 1, 1)" + elif signal1_belt == 'B' and signal2_belt == 'A': + signal1_belt += " (axis 1, 1)" + signal2_belt += " (axis 1,-1)" + else: + print("Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)") + + # Plot the two belts PSD signals + ax.plot(signal1.freqs, signal1.psd, label="Belt " + signal1_belt, color=KLIPPAIN_COLORS['purple']) + ax.plot(signal2.freqs, signal2.psd, label="Belt " + signal2_belt, color=KLIPPAIN_COLORS['orange']) + + # Trace the "relax region" (also used as a threshold to filter and detect the peaks) + psd_lowest_max = min(signal1.psd.max(), signal2.psd.max()) + peaks_warning_threshold = PEAKS_DETECTION_THRESHOLD * psd_lowest_max + ax.axhline(y=peaks_warning_threshold, color='black', linestyle='--', linewidth=0.5) + ax.fill_between(signal1.freqs, 0, peaks_warning_threshold, color='green', alpha=0.15, label='Relax Region') + + # Trace and annotate the peaks on the graph + paired_peak_count = 0 + unpaired_peak_count = 0 + offsets_table_data = [] + + for _, (peak1, peak2) in enumerate(signal1.paired_peaks): + label = ALPHABET[paired_peak_count] + amplitude_offset = abs(((signal2.psd[peak2[0]] - signal1.psd[peak1[0]]) / max(signal1.psd[peak1[0]], signal2.psd[peak2[0]])) * 100) + frequency_offset = abs(signal2.freqs[peak2[0]] - signal1.freqs[peak1[0]]) + offsets_table_data.append([f"Peaks {label}", f"{frequency_offset:.1f} Hz", f"{amplitude_offset:.1f} %"]) + + ax.plot(signal1.freqs[peak1[0]], signal1.psd[peak1[0]], "x", color='black') + ax.plot(signal2.freqs[peak2[0]], signal2.psd[peak2[0]], "x", color='black') + ax.plot([signal1.freqs[peak1[0]], signal2.freqs[peak2[0]]], [signal1.psd[peak1[0]], signal2.psd[peak2[0]]], ":", color='gray') + + ax.annotate(label + "1", (signal1.freqs[peak1[0]], signal1.psd[peak1[0]]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='black') + ax.annotate(label + "2", (signal2.freqs[peak2[0]], signal2.psd[peak2[0]]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='black') + paired_peak_count += 1 + + for peak in signal1.unpaired_peaks: + ax.plot(signal1.freqs[peak], signal1.psd[peak], "x", color='black') + ax.annotate(str(unpaired_peak_count + 1), (signal1.freqs[peak], signal1.psd[peak]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='red', weight='bold') + unpaired_peak_count += 1 + + for peak in signal2.unpaired_peaks: + ax.plot(signal2.freqs[peak], signal2.psd[peak], "x", color='black') + ax.annotate(str(unpaired_peak_count + 1), (signal2.freqs[peak], signal2.psd[peak]), + textcoords="offset points", xytext=(8, 5), + ha='left', fontsize=13, color='red', weight='bold') + unpaired_peak_count += 1 + + # Compute the similarity (using cross-correlation of the PSD signals) + ax2 = ax.twinx() # To split the legends in two box + ax2.yaxis.set_visible(False) + similarity_factor = compute_curve_similarity_factor(signal1, signal2) + ax2.plot([], [], ' ', label=f'Estimated similarity: {similarity_factor:.1f}%') + ax2.plot([], [], ' ', label=f'Number of unpaired peaks: {unpaired_peak_count}') + print(f"Belts estimated similarity: {similarity_factor:.1f}%") + + # Setting axis parameters, grid and graph title + ax.set_xlabel('Frequency (Hz)') + ax.set_xlim([0, max_freq]) + ax.set_ylabel('Power spectral density') + psd_highest_max = max(signal1.psd.max(), signal2.psd.max()) + ax.set_ylim([0, psd_highest_max + psd_highest_max * 0.05]) + + ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) + ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0)) + ax.grid(which='major', color='grey') + ax.grid(which='minor', color='lightgrey') + fontP = matplotlib.font_manager.FontProperties() + fontP.set_size('small') + ax.set_title('Belts Frequency Profiles (estimated similarity: {:.1f}%)'.format(similarity_factor), fontsize=10, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + + # Print the table of offsets ontop of the graph below the original legend (upper right) + if len(offsets_table_data) > 0: + columns = ["", "Frequency delta", "Amplitude delta", ] + offset_table = ax.table(cellText=offsets_table_data, colLabels=columns, bbox=[0.66, 0.75, 0.33, 0.15], loc='upper right', cellLoc='center') + offset_table.auto_set_font_size(False) + offset_table.set_fontsize(8) + offset_table.auto_set_column_width([0, 1, 2]) + offset_table.set_zorder(100) + cells = [key for key in offset_table.get_celld().keys()] + for cell in cells: + offset_table[cell].set_facecolor('white') + offset_table[cell].set_alpha(0.6) + + ax.legend(loc='upper left', prop=fontP) + ax2.legend(loc='center right', prop=fontP) + + return similarity_factor, unpaired_peak_count + + +def plot_difference_spectrogram(ax, data1, data2, signal1, signal2, similarity_factor, max_freq): + combined_sum, combined_divergent, bins, t = combined_spectrogram(data1, data2) + + # Compute the MHI value from the differential spectrogram sum of gradient, salted with + # the similarity factor and the number or unpaired peaks from the belts frequency profile + # Be careful, this value is highly opinionated and is pretty experimental! + mhi, textual_mhi = compute_mhi(combined_sum, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks)) + print(f"[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)") + ax.set_title(f"Differential Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') + ax.plot([], [], ' ', label=f'{textual_mhi} (experimental)') + + # Draw the differential spectrogram with a specific custom norm to get orange or purple values where there is signal or white near zeros + colors = [KLIPPAIN_COLORS['dark_orange'], KLIPPAIN_COLORS['orange'], 'white', KLIPPAIN_COLORS['purple'], KLIPPAIN_COLORS['dark_purple']] + cm = matplotlib.colors.LinearSegmentedColormap.from_list('klippain_divergent', list(zip([0, 0.25, 0.5, 0.75, 1], colors))) + norm = matplotlib.colors.TwoSlopeNorm(vmin=np.min(combined_divergent), vcenter=0, vmax=np.max(combined_divergent)) + ax.pcolormesh(t, bins, combined_divergent.T, cmap=cm, norm=norm, shading='gouraud') + + ax.set_xlabel('Frequency (hz)') + ax.set_xlim([0., max_freq]) + ax.set_ylabel('Time (s)') + ax.set_ylim([0, bins[-1]]) + + fontP = matplotlib.font_manager.FontProperties() + fontP.set_size('medium') + ax.legend(loc='best', prop=fontP) + + # Plot vertical lines for unpaired peaks + unpaired_peak_count = 0 + for _, peak in enumerate(signal1.unpaired_peaks): + ax.axvline(signal1.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5) + ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal1.freqs[peak], t[-1]*0.05), + textcoords="data", color=KLIPPAIN_COLORS['red_pink'], rotation=90, fontsize=10, + verticalalignment='bottom', horizontalalignment='right') + unpaired_peak_count +=1 + + for _, peak in enumerate(signal2.unpaired_peaks): + ax.axvline(signal2.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5) + ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal2.freqs[peak], t[-1]*0.05), + textcoords="data", color=KLIPPAIN_COLORS['red_pink'], rotation=90, fontsize=10, + verticalalignment='bottom', horizontalalignment='right') + unpaired_peak_count +=1 + + # Plot vertical lines and zones for paired peaks + for idx, (peak1, peak2) in enumerate(signal1.paired_peaks): + label = ALPHABET[idx] + x_min = min(peak1[1], peak2[1]) + x_max = max(peak1[1], peak2[1]) + ax.axvline(x_min, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5) + ax.axvline(x_max, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5) + ax.fill_between([x_min, x_max], 0, np.max(combined_divergent), color=KLIPPAIN_COLORS['dark_purple'], alpha=0.3) + ax.annotate(f"Peaks {label}", (x_min, t[-1]*0.05), + textcoords="data", color=KLIPPAIN_COLORS['dark_purple'], rotation=90, fontsize=10, + verticalalignment='bottom', horizontalalignment='right') + + return + + +###################################################################### +# Custom tools +###################################################################### + +# Simple helper to compute a sigmoid scalling (from 0 to 100%) +def sigmoid_scale(x, k=1): + return 1 / (1 + np.exp(-k * x)) * 100 + +# Original Klipper function to get the PSD data of a raw accelerometer signal +def compute_signal_data(data, max_freq): + calibration_data = calc_freq_response(data) + freqs = calibration_data.freq_bins[calibration_data.freq_bins <= max_freq] + psd = calibration_data.get_psd('all')[calibration_data.freq_bins <= max_freq] + peaks, _ = detect_peaks(psd, freqs) + return SignalData(freqs=freqs, psd=psd, peaks=peaks, paired_peaks=None, unpaired_peaks=None) + + +###################################################################### +# Startup and main routines +###################################################################### + +def parse_log(logname): + with open(logname) as f: + for header in f: + if not header.startswith('#'): + break + if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'): + # Raw accelerometer data + return np.loadtxt(logname, comments='#', delimiter=',') + # Power spectral density data or shaper calibration data + raise ValueError("File %s does not contain raw accelerometer data and therefore " + "is not supported by this script. Please use the official Klipper " + "graph_accelerometer.py script to process it instead." % (logname,)) + + +def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200., graph_spectogram=True, width=8.3, height=11.6): + for filename in lognames[:2]: + # Wait for the file handler to be released by Klipper + while is_file_open(filename): + time.sleep(2) + + # Parse data + datas = [parse_log(fn) for fn in lognames] + if len(datas) > 2: + raise ValueError("Incorrect number of .csv files used (this function needs two files to compare them)") + + # Compute calibration data for the two datasets with automatic peaks detection + signal1 = compute_signal_data(datas[0], max_freq) + signal2 = compute_signal_data(datas[1], max_freq) + + # Pair the peaks across the two datasets + paired_peaks, unpaired_peaks1, unpaired_peaks2 = pair_peaks(signal1.peaks, signal1.freqs, signal1.psd, + signal2.peaks, signal2.freqs, signal2.psd) + signal1 = signal1._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks1) + signal2 = signal2._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks2) + + + if graph_spectogram: + fig = matplotlib.pyplot.figure() + gs = matplotlib.gridspec.GridSpec(2, 1, height_ratios=[4, 3]) + ax1 = fig.add_subplot(gs[0]) + ax2 = fig.add_subplot(gs[1]) + else: + fig, ax1 = matplotlib.pyplot.subplots() + + # Add title + try: + filename = lognames[0].split('/')[-1] + dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", "%Y%m%d %H%M%S") + title_line2 = dt.strftime('%x %X') + except: + print("Warning: CSV filenames look to be different than expected (%s , %s)" % (lognames[0], lognames[1])) + title_line2 = lognames[0].split('/')[-1] + " / " + lognames[1].split('/')[-1] + fig.suptitle(title_line2) + + # Plot the graphs + similarity_factor, _ = plot_compare_frequency(ax1, lognames, signal1, signal2, max_freq) + if graph_spectogram: + plot_difference_spectrogram(ax2, datas[0], datas[1], signal1, signal2, similarity_factor, max_freq) + + fig.set_size_inches(width, height) + fig.tight_layout() + fig.subplots_adjust(top=0.89) + + return fig + + +def main(): + # Parse command-line arguments + usage = "%prog [options] " + opts = optparse.OptionParser(usage) + opts.add_option("-o", "--output", type="string", dest="output", + default=None, help="filename of output graph") + opts.add_option("-f", "--max_freq", type="float", default=200., + help="maximum frequency to graph") + opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir", + default="~/klipper", help="main klipper directory") + opts.add_option("-n", "--no_spectogram", action="store_false", dest="no_spectogram", + default=True, help="disable plotting of spectogram") + opts.add_option("-w", "--width", type="float", dest="width", + default=8.3, help="width (inches) of the graph(s)") + opts.add_option("-l", "--height", type="float", dest="height", + default=11.6, help="height (inches) of the graph(s)") + + options, args = opts.parse_args() + if len(args) < 1: + opts.error("Incorrect number of arguments") + if options.output is None: + opts.error("You must specify an output file.png to use the script (option -o)") + + fig = belts_calibration(args, options.klipperdir, options.max_freq, options.no_spectogram, + options.width, options.height) + pathlib.Path(options.output).unlink(missing_ok=True) + fig.savefig(options.output) + + +if __name__ == '__main__': + main() diff --git a/files/improved-shapers/scripts/shaper_calibrate.py b/files/improved-shapers/scripts/shaper_calibrate.py new file mode 100755 index 0000000..b9b4c66 --- /dev/null +++ b/files/improved-shapers/scripts/shaper_calibrate.py @@ -0,0 +1,372 @@ +# Automatic calibration of input shapers +# +# Copyright (C) 2020 Dmitry Butyugin +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import collections, importlib, logging, math, multiprocessing, traceback +import shaper_defs + +MIN_FREQ = 5. +MAX_FREQ = 200. +WINDOW_T_SEC = 0.5 +MAX_SHAPER_FREQ = 150. + +TEST_DAMPING_RATIOS=[0.075, 0.1, 0.15] + +AUTOTUNE_SHAPERS = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei'] + +###################################################################### +# Frequency response calculation and shaper auto-tuning +###################################################################### + +class CalibrationData: + def __init__(self, freq_bins, psd_sum, psd_x, psd_y, psd_z): + self.freq_bins = freq_bins + self.psd_sum = psd_sum + self.psd_x = psd_x + self.psd_y = psd_y + self.psd_z = psd_z + self._psd_list = [self.psd_sum, self.psd_x, self.psd_y, self.psd_z] + self._psd_map = {'x': self.psd_x, 'y': self.psd_y, 'z': self.psd_z, + 'all': self.psd_sum} + self.data_sets = 1 + def add_data(self, other): + np = self.numpy + joined_data_sets = self.data_sets + other.data_sets + for psd, other_psd in zip(self._psd_list, other._psd_list): + # `other` data may be defined at different frequency bins, + # interpolating to fix that. + other_normalized = other.data_sets * np.interp( + self.freq_bins, other.freq_bins, other_psd) + psd *= self.data_sets + psd[:] = (psd + other_normalized) * (1. / joined_data_sets) + self.data_sets = joined_data_sets + def set_numpy(self, numpy): + self.numpy = numpy + def normalize_to_frequencies(self): + for psd in self._psd_list: + # Avoid division by zero errors + psd /= self.freq_bins + .1 + # Remove low-frequency noise + psd[self.freq_bins < MIN_FREQ] = 0. + def get_psd(self, axis='all'): + return self._psd_map[axis] + + +CalibrationResult = collections.namedtuple( + 'CalibrationResult', + ('name', 'freq', 'vals', 'vibrs', 'smoothing', 'score', 'max_accel')) + +class ShaperCalibrate: + def __init__(self, printer): + self.printer = printer + self.error = printer.command_error if printer else Exception + try: + self.numpy = importlib.import_module('numpy') + except ImportError: + raise self.error( + "Failed to import `numpy` module, make sure it was " + "installed via `~/klippy-env/bin/pip install` (refer to " + "docs/Measuring_Resonances.md for more details).") + + def background_process_exec(self, method, args): + if self.printer is None: + return method(*args) + import queuelogger + parent_conn, child_conn = multiprocessing.Pipe() + def wrapper(): + queuelogger.clear_bg_logging() + try: + res = method(*args) + except: + child_conn.send((True, traceback.format_exc())) + child_conn.close() + return + child_conn.send((False, res)) + child_conn.close() + # Start a process to perform the calculation + calc_proc = multiprocessing.Process(target=wrapper) + calc_proc.daemon = True + calc_proc.start() + # Wait for the process to finish + reactor = self.printer.get_reactor() + gcode = self.printer.lookup_object("gcode") + eventtime = last_report_time = reactor.monotonic() + while calc_proc.is_alive(): + if eventtime > last_report_time + 5.: + last_report_time = eventtime + gcode.respond_info("Wait for calculations..", log=False) + eventtime = reactor.pause(eventtime + .1) + # Return results + is_err, res = parent_conn.recv() + if is_err: + raise self.error("Error in remote calculation: %s" % (res,)) + calc_proc.join() + parent_conn.close() + return res + + def _split_into_windows(self, x, window_size, overlap): + # Memory-efficient algorithm to split an input 'x' into a series + # of overlapping windows + step_between_windows = window_size - overlap + n_windows = (x.shape[-1] - overlap) // step_between_windows + shape = (window_size, n_windows) + strides = (x.strides[-1], step_between_windows * x.strides[-1]) + return self.numpy.lib.stride_tricks.as_strided( + x, shape=shape, strides=strides, writeable=False) + + def _psd(self, x, fs, nfft): + # Calculate power spectral density (PSD) using Welch's algorithm + np = self.numpy + window = np.kaiser(nfft, 6.) + # Compensation for windowing loss + scale = 1.0 / (window**2).sum() + + # Split into overlapping windows of size nfft + overlap = nfft // 2 + x = self._split_into_windows(x, nfft, overlap) + + # First detrend, then apply windowing function + x = window[:, None] * (x - np.mean(x, axis=0)) + + # Calculate frequency response for each window using FFT + result = np.fft.rfft(x, n=nfft, axis=0) + result = np.conjugate(result) * result + result *= scale / fs + # For one-sided FFT output the response must be doubled, except + # the last point for unpaired Nyquist frequency (assuming even nfft) + # and the 'DC' term (0 Hz) + result[1:-1,:] *= 2. + + # Welch's algorithm: average response over windows + psd = result.real.mean(axis=-1) + + # Calculate the frequency bins + freqs = np.fft.rfftfreq(nfft, 1. / fs) + return freqs, psd + + def calc_freq_response(self, raw_values): + np = self.numpy + if raw_values is None: + return None + if isinstance(raw_values, np.ndarray): + data = raw_values + else: + samples = raw_values.get_samples() + if not samples: + return None + data = np.array(samples) + + N = data.shape[0] + T = data[-1,0] - data[0,0] + SAMPLING_FREQ = N / T + # Round up to the nearest power of 2 for faster FFT + M = 1 << int(SAMPLING_FREQ * WINDOW_T_SEC - 1).bit_length() + if N <= M: + return None + + # Calculate PSD (power spectral density) of vibrations per + # frequency bins (the same bins for X, Y, and Z) + fx, px = self._psd(data[:,1], SAMPLING_FREQ, M) + fy, py = self._psd(data[:,2], SAMPLING_FREQ, M) + fz, pz = self._psd(data[:,3], SAMPLING_FREQ, M) + return CalibrationData(fx, px+py+pz, px, py, pz) + + def process_accelerometer_data(self, data): + calibration_data = self.background_process_exec( + self.calc_freq_response, (data,)) + if calibration_data is None: + raise self.error( + "Internal error processing accelerometer data %s" % (data,)) + calibration_data.set_numpy(self.numpy) + return calibration_data + + def _estimate_shaper(self, shaper, test_damping_ratio, test_freqs): + np = self.numpy + + A, T = np.array(shaper[0]), np.array(shaper[1]) + inv_D = 1. / A.sum() + + omega = 2. * math.pi * test_freqs + damping = test_damping_ratio * omega + omega_d = omega * math.sqrt(1. - test_damping_ratio**2) + W = A * np.exp(np.outer(-damping, (T[-1] - T))) + S = W * np.sin(np.outer(omega_d, T)) + C = W * np.cos(np.outer(omega_d, T)) + return np.sqrt(S.sum(axis=1)**2 + C.sum(axis=1)**2) * inv_D + + def _estimate_remaining_vibrations(self, shaper, test_damping_ratio, + freq_bins, psd): + vals = self._estimate_shaper(shaper, test_damping_ratio, freq_bins) + # The input shaper can only reduce the amplitude of vibrations by + # SHAPER_VIBRATION_REDUCTION times, so all vibrations below that + # threshold can be igonred + vibr_threshold = psd.max() / shaper_defs.SHAPER_VIBRATION_REDUCTION + remaining_vibrations = self.numpy.maximum( + vals * psd - vibr_threshold, 0).sum() + all_vibrations = self.numpy.maximum(psd - vibr_threshold, 0).sum() + return (remaining_vibrations / all_vibrations, vals) + + def _get_shaper_smoothing(self, shaper, accel=5000, scv=5.): + half_accel = accel * .5 + + A, T = shaper + inv_D = 1. / sum(A) + n = len(T) + # Calculate input shaper shift + ts = sum([A[i] * T[i] for i in range(n)]) * inv_D + + # Calculate offset for 90 and 180 degrees turn + offset_90 = offset_180 = 0. + for i in range(n): + if T[i] >= ts: + # Calculate offset for one of the axes + offset_90 += A[i] * (scv + half_accel * (T[i]-ts)) * (T[i]-ts) + offset_180 += A[i] * half_accel * (T[i]-ts)**2 + offset_90 *= inv_D * math.sqrt(2.) + offset_180 *= inv_D + return max(offset_90, offset_180) + + def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing): + np = self.numpy + + test_freqs = np.arange(shaper_cfg.min_freq, MAX_SHAPER_FREQ, .2) + + freq_bins = calibration_data.freq_bins + psd = calibration_data.psd_sum[freq_bins <= MAX_FREQ] + freq_bins = freq_bins[freq_bins <= MAX_FREQ] + + best_res = None + results = [] + for test_freq in test_freqs[::-1]: + shaper_vibrations = 0. + shaper_vals = np.zeros(shape=freq_bins.shape) + shaper = shaper_cfg.init_func( + test_freq, shaper_defs.DEFAULT_DAMPING_RATIO) + shaper_smoothing = self._get_shaper_smoothing(shaper) + if max_smoothing and shaper_smoothing > max_smoothing and best_res: + return best_res + # Exact damping ratio of the printer is unknown, pessimizing + # remaining vibrations over possible damping values + for dr in TEST_DAMPING_RATIOS: + vibrations, vals = self._estimate_remaining_vibrations( + shaper, dr, freq_bins, psd) + shaper_vals = np.maximum(shaper_vals, vals) + if vibrations > shaper_vibrations: + shaper_vibrations = vibrations + max_accel = self.find_shaper_max_accel(shaper) + # The score trying to minimize vibrations, but also accounting + # the growth of smoothing. The formula itself does not have any + # special meaning, it simply shows good results on real user data + shaper_score = shaper_smoothing * (shaper_vibrations**1.5 + + shaper_vibrations * .2 + .01) + results.append( + CalibrationResult( + name=shaper_cfg.name, freq=test_freq, vals=shaper_vals, + vibrs=shaper_vibrations, smoothing=shaper_smoothing, + score=shaper_score, max_accel=max_accel)) + if best_res is None or best_res.vibrs > results[-1].vibrs: + # The current frequency is better for the shaper. + best_res = results[-1] + # Try to find an 'optimal' shapper configuration: the one that is not + # much worse than the 'best' one, but gives much less smoothing + selected = best_res + for res in results[::-1]: + if res.vibrs < best_res.vibrs * 1.1 and res.score < selected.score: + selected = res + return selected + + def _bisect(self, func): + left = right = 1. + while not func(left): + right = left + left *= .5 + if right == left: + while func(right): + right *= 2. + while right - left > 1e-8: + middle = (left + right) * .5 + if func(middle): + left = middle + else: + right = middle + return left + + def find_shaper_max_accel(self, shaper): + # Just some empirically chosen value which produces good projections + # for max_accel without much smoothing + TARGET_SMOOTHING = 0.12 + max_accel = self._bisect(lambda test_accel: self._get_shaper_smoothing( + shaper, test_accel) <= TARGET_SMOOTHING) + return max_accel + + def find_best_shaper(self, calibration_data, max_smoothing, logger=None): + best_shaper = None + all_shapers = [] + resp = {} + for shaper_cfg in shaper_defs.INPUT_SHAPERS: + if shaper_cfg.name not in AUTOTUNE_SHAPERS: + continue + shaper = self.background_process_exec(self.fit_shaper, ( + shaper_cfg, calibration_data, max_smoothing)) + if logger is not None: + resp[shaper.name] = { + 'freq': shaper.freq, + 'vib': shaper.vibrs * 100., + 'smooth': shaper.smoothing, + 'max_acel': round(shaper.max_accel / 100.) * 100. + } + all_shapers.append(shaper) + if (best_shaper is None or shaper.score * 1.2 < best_shaper.score or + (shaper.score * 1.05 < best_shaper.score and + shaper.smoothing * 1.1 < best_shaper.smoothing)): + # Either the shaper significantly improves the score (by 20%), + # or it improves the score and smoothing (by 5% and 10% resp.) + best_shaper = shaper + return best_shaper, all_shapers, {'shapers': resp, 'best': best_shaper.name} + + def save_params(self, configfile, axis, shaper_name, shaper_freq): + if axis == 'xy': + self.save_params(configfile, 'x', shaper_name, shaper_freq) + self.save_params(configfile, 'y', shaper_name, shaper_freq) + else: + configfile.set('input_shaper', 'shaper_type_'+axis, shaper_name) + configfile.set('input_shaper', 'shaper_freq_'+axis, + '%.1f' % (shaper_freq,)) + + def apply_params(self, input_shaper, axis, shaper_name, shaper_freq): + if axis == 'xy': + self.apply_params(input_shaper, 'x', shaper_name, shaper_freq) + self.apply_params(input_shaper, 'y', shaper_name, shaper_freq) + return + gcode = self.printer.lookup_object("gcode") + axis = axis.upper() + input_shaper.cmd_SET_INPUT_SHAPER(gcode.create_gcode_command( + "SET_INPUT_SHAPER", "SET_INPUT_SHAPER", { + "SHAPER_TYPE_" + axis: shaper_name, + "SHAPER_FREQ_" + axis: shaper_freq})) + + def save_calibration_data(self, output, calibration_data, shapers=None): + try: + with open(output, "w") as csvfile: + csvfile.write("freq,psd_x,psd_y,psd_z,psd_xyz") + if shapers: + for shaper in shapers: + csvfile.write(",%s(%.1f)" % (shaper.name, shaper.freq)) + csvfile.write("\n") + num_freqs = calibration_data.freq_bins.shape[0] + for i in range(num_freqs): + if calibration_data.freq_bins[i] >= MAX_FREQ: + break + csvfile.write("%.1f,%.3e,%.3e,%.3e,%.3e" % ( + calibration_data.freq_bins[i], + calibration_data.psd_x[i], + calibration_data.psd_y[i], + calibration_data.psd_z[i], + calibration_data.psd_sum[i])) + if shapers: + for shaper in shapers: + csvfile.write(",%.3f" % (shaper.vals[i],)) + csvfile.write("\n") + except IOError as e: + raise self.error("Error writing to file '%s': %s", output, str(e)) diff --git a/files/improved-shapers/scripts/shaper_defs.py b/files/improved-shapers/scripts/shaper_defs.py new file mode 100755 index 0000000..611fed1 --- /dev/null +++ b/files/improved-shapers/scripts/shaper_defs.py @@ -0,0 +1,102 @@ +# Definitions of the supported input shapers +# +# Copyright (C) 2020-2021 Dmitry Butyugin +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import collections, math + +SHAPER_VIBRATION_REDUCTION=20. +DEFAULT_DAMPING_RATIO = 0.1 + +InputShaperCfg = collections.namedtuple( + 'InputShaperCfg', ('name', 'init_func', 'min_freq')) + +def get_none_shaper(): + return ([], []) + +def get_zv_shaper(shaper_freq, damping_ratio): + df = math.sqrt(1. - damping_ratio**2) + K = math.exp(-damping_ratio * math.pi / df) + t_d = 1. / (shaper_freq * df) + A = [1., K] + T = [0., .5*t_d] + return (A, T) + +def get_zvd_shaper(shaper_freq, damping_ratio): + df = math.sqrt(1. - damping_ratio**2) + K = math.exp(-damping_ratio * math.pi / df) + t_d = 1. / (shaper_freq * df) + A = [1., 2.*K, K**2] + T = [0., .5*t_d, t_d] + return (A, T) + +def get_mzv_shaper(shaper_freq, damping_ratio): + df = math.sqrt(1. - damping_ratio**2) + K = math.exp(-.75 * damping_ratio * math.pi / df) + t_d = 1. / (shaper_freq * df) + + a1 = 1. - 1. / math.sqrt(2.) + a2 = (math.sqrt(2.) - 1.) * K + a3 = a1 * K * K + + A = [a1, a2, a3] + T = [0., .375*t_d, .75*t_d] + return (A, T) + +def get_ei_shaper(shaper_freq, damping_ratio): + v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance + df = math.sqrt(1. - damping_ratio**2) + K = math.exp(-damping_ratio * math.pi / df) + t_d = 1. / (shaper_freq * df) + + a1 = .25 * (1. + v_tol) + a2 = .5 * (1. - v_tol) * K + a3 = a1 * K * K + + A = [a1, a2, a3] + T = [0., .5*t_d, t_d] + return (A, T) + +def get_2hump_ei_shaper(shaper_freq, damping_ratio): + v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance + df = math.sqrt(1. - damping_ratio**2) + K = math.exp(-damping_ratio * math.pi / df) + t_d = 1. / (shaper_freq * df) + + V2 = v_tol**2 + X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.) + a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X) + a2 = (.5 - a1) * K + a3 = a2 * K + a4 = a1 * K * K * K + + A = [a1, a2, a3, a4] + T = [0., .5*t_d, t_d, 1.5*t_d] + return (A, T) + +def get_3hump_ei_shaper(shaper_freq, damping_ratio): + v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance + df = math.sqrt(1. - damping_ratio**2) + K = math.exp(-damping_ratio * math.pi / df) + t_d = 1. / (shaper_freq * df) + + K2 = K*K + a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol)) + a2 = 0.25 * (1. - v_tol) * K + a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2 + a4 = a2 * K2 + a5 = a1 * K2 * K2 + + A = [a1, a2, a3, a4, a5] + T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d] + return (A, T) + +# min_freq for each shaper is chosen to have projected max_accel ~= 1500 +INPUT_SHAPERS = [ + InputShaperCfg('zv', get_zv_shaper, min_freq=21.), + InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.), + InputShaperCfg('zvd', get_zvd_shaper, min_freq=29.), + InputShaperCfg('ei', get_ei_shaper, min_freq=29.), + InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.), + InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.), +] diff --git a/files/kamp/Adaptive_Meshing.cfg b/files/kamp/Adaptive_Meshing.cfg new file mode 100644 index 0000000..0b599ae --- /dev/null +++ b/files/kamp/Adaptive_Meshing.cfg @@ -0,0 +1,93 @@ +########################################### +# Adaptive Meshing for Creality K1 Series +########################################### + +[gcode_macro BED_MESH_CALIBRATE] +rename_existing: _BED_MESH_CALIBRATE +gcode: + {% set all_points = printer.exclude_object.objects | map(attribute='polygon') | sum(start=[]) %} + {% set bed_mesh_min = printer.configfile.settings.bed_mesh.mesh_min %} + {% set bed_mesh_max = printer.configfile.settings.bed_mesh.mesh_max %} + {% set probe_count = printer.configfile.settings.bed_mesh.probe_count %} + {% set kamp_settings = printer["gcode_macro _KAMP_Settings"] %} + {% set verbose_enable = kamp_settings.verbose_enable | abs %} + {% set mesh_margin = kamp_settings.mesh_margin | float %} + {% set fuzz_amount = kamp_settings.fuzz_amount | float %} + {% set probe_count = probe_count if probe_count|length > 1 else probe_count * 2 %} + {% set max_probe_point_distance_x = ( bed_mesh_max[0] - bed_mesh_min[0] ) / (probe_count[0] - 1) %} + {% set max_probe_point_distance_y = ( bed_mesh_max[1] - bed_mesh_min[1] ) / (probe_count[1] - 1) %} + {% set x_min = all_points | map(attribute=0) | min | default(bed_mesh_min[0]) %} + {% set y_min = all_points | map(attribute=1) | min | default(bed_mesh_min[1]) %} + {% set x_max = all_points | map(attribute=0) | max | default(bed_mesh_max[0]) %} + {% set y_max = all_points | map(attribute=1) | max | default(bed_mesh_max[1]) %} + {% set fuzz_range = range((0) | int, (fuzz_amount * 100) | int + 1) %} + {% set adapted_x_min = x_min - mesh_margin - (fuzz_range | random / 100.0) %} + {% set adapted_y_min = y_min - mesh_margin - (fuzz_range | random / 100.0) %} + {% set adapted_x_max = x_max + mesh_margin + (fuzz_range | random / 100.0) %} + {% set adapted_y_max = y_max + mesh_margin + (fuzz_range | random / 100.0) %} + {% set adapted_x_min = [adapted_x_min , bed_mesh_min[0]] | max %} + {% set adapted_y_min = [adapted_y_min , bed_mesh_min[1]] | max %} + {% set adapted_x_max = [adapted_x_max , bed_mesh_max[0]] | min %} + {% set adapted_y_max = [adapted_y_max , bed_mesh_max[1]] | min %} + {% set points_x = (((adapted_x_max - adapted_x_min) / max_probe_point_distance_x) | round(method='ceil') | int) + 1 %} + {% set points_y = (((adapted_y_max - adapted_y_min) / max_probe_point_distance_y) | round(method='ceil') | int) + 1 %} + {% if (points_x > points_y) %} + {% set points_y = points_x %} + {% endif %} + {% if (points_x < points_y) %} + {% set points_x = points_y %} + {% endif %} + {% if (([points_x, points_y]|max) > 6) %} + {% set algorithm = "bicubic" %} + {% set min_points = 4 %} + {% else %} + {% set algorithm = "lagrange" %} + {% set min_points = 3 %} + {% endif %} + {% set points_x = [points_x , min_points]|max %} + {% set points_y = [points_y , min_points]|max %} + {% set points_x = [points_x , probe_count[0]]|min %} + {% set points_y = [points_y , probe_count[1]]|min %} + + {% if verbose_enable == True %} + + {% if printer.exclude_object.objects != [] %} + + RESPOND TYPE=command MSG="Algorithm: {algorithm}" + RESPOND TYPE=command MSG="Default probe count: {probe_count[0]},{probe_count[1]}" + RESPOND TYPE=command MSG="Adapted probe count: {points_x},{points_y}" + RESPOND TYPE=command MSG="Default mesh bounds: {bed_mesh_min[0]},{bed_mesh_min[1]}, {bed_mesh_max[0]},{bed_mesh_max[1]}" + + {% if mesh_margin > 0 %} + + RESPOND TYPE=command MSG="Mesh margin is {mesh_margin}, mesh bounds extended by {mesh_margin}mm." + + {% else %} + + RESPOND TYPE=command MSG="Mesh margin is 0, margin not increased." + + {% endif %} + + {% if fuzz_amount > 0 %} + + RESPOND TYPE=command MSG="Mesh point fuzzing enabled, points fuzzed up to {fuzz_amount}mm" + + {% else %} + + RESPOND TYPE=command MSG="Fuzz amount is 0, mesh points not fuzzed." + + {% endif %} + + RESPOND TYPE=command MSG="Adapted mesh bounds: {adapted_x_min},{adapted_y_min}, {adapted_x_max},{adapted_y_max}" + RESPOND TYPE=command MSG="KAMP adjustments successful. Happy KAMPing!" + + {% else %} + + RESPOND TYPE=command MSG="No object detected! Make sure you have enabled Exclude Objets setting in your slicer. Using Full Bed Mesh." + G4 P5000 + + {% endif %} + + {% endif %} + + _BED_MESH_CALIBRATE mesh_min={adapted_x_min},{adapted_y_min} mesh_max={adapted_x_max},{adapted_y_max} ALGORITHM={algorithm} PROBE_COUNT={points_x},{points_y} diff --git a/files/kamp/KAMP_Settings.cfg b/files/kamp/KAMP_Settings.cfg new file mode 100644 index 0000000..62fe6cd --- /dev/null +++ b/files/kamp/KAMP_Settings.cfg @@ -0,0 +1,40 @@ +########################################### +# KAMP Settings for Creality K1 Series +########################################### + +# Below you can enable or disable specific configuration files depending on what you want KAMP to do: + +[include Start_Print.cfg] # START_PRINT macro for Creality K1 Series. +[include Adaptive_Meshing.cfg] # Adaptive Meshing configurations. +[include Line_Purge.cfg] # Adaptive Line Purging configurations. +[include Smart_Park.cfg] # Smart Park feature, which parks the printhead near the print area for final heating. +#[include Prusa_Slicer.cfg] # Enable this if you use Prusa Slicer, it's the necessary macros to enable Exclude Objects functionality. + +[respond] # Necessary to receive messages from KAMP + +[gcode_macro _KAMP_Settings] +description: This macro contains all adjustable settings for KAMP + +# The following variables are settings for KAMP as a whole: + +variable_verbose_enable: True # Set to True to enable KAMP information output when running. This is useful for debugging. + +# The following variables are for adjusting Adaptive Meshing settings for KAMP: + +variable_mesh_margin: 0 # Expands the mesh size in millimeters if desired. Leave at 0 to disable. +variable_fuzz_amount: 0 # Slightly randomizes mesh points to spread out wear from nozzle-based probes. Leave at 0 to disable. + +# The following variables are for adjusting Adaptive Line Purging settings for KAMP: + +variable_purge_height: 0.8 # Z position of nozzle during purge. Default is 0.8. +variable_tip_distance: 0 # Distance between tip of filament and nozzle before purge. Should be similar to PRINT_END final retract amount. Default is 0. +variable_purge_margin: 10 # Distance the purge will be in front of the print area. Default is 10. +variable_purge_amount: 50 # Amount of filament to be purged prior to printing. Default is 50. +variable_flow_rate: 12 # Flow rate of purge in mm3/s. Default is 12. + +# The following variables are for adjusting the Smart Park feature for KAMP, which will park the printhead near the print area at a specified height: + +variable_smart_park_height: 10 # Z position for Smart Park. Default is 10. + +gcode: + RESPOND TYPE=command MSG="Running the KAMP_Settings macro does nothing, it's only used for storing KAMP settings." diff --git a/files/kamp/Line_Purge.cfg b/files/kamp/Line_Purge.cfg new file mode 100644 index 0000000..c998fb3 --- /dev/null +++ b/files/kamp/Line_Purge.cfg @@ -0,0 +1,200 @@ +########################################### +# Line Purge for Creality K1 Series +########################################### + +[gcode_macro _LINE_PURGE] +description: A purge macro that adapts to be near your actual printed objects +gcode: + {% set travel_speed = (printer.toolhead.max_velocity) * 60 | float %} + {% set cross_section = printer.configfile.settings.extruder.max_extrude_cross_section | float %} + {% if printer.firmware_retraction is defined %} + {% set RETRACT = G10 | string %} + {% set UNRETRACT = G11 | string %} + {% else %} + {% set RETRACT = 'G1 E-0.5 F2400' | string %} + {% set UNRETRACT = 'G1 E0.5 F2400' | string %} + {% endif %} + {% set bed_x_max = printer["gcode_macro PRINTER_PARAM"].max_x_position | float %} + {% set bed_y_max = printer["gcode_macro PRINTER_PARAM"].max_y_position | float %} + {% set verbose_enable = printer["gcode_macro _KAMP_Settings"].verbose_enable | abs %} + {% set purge_height = printer["gcode_macro _KAMP_Settings"].purge_height | float %} + {% set tip_distance = printer["gcode_macro _KAMP_Settings"].tip_distance | float %} + {% set purge_margin = printer["gcode_macro _KAMP_Settings"].purge_margin | float %} + {% set purge_amount = printer["gcode_macro _KAMP_Settings"].purge_amount | float %} + {% set flow_rate = printer["gcode_macro _KAMP_Settings"].flow_rate | float %} + {% set rapid_move = 10 %} + {% set all_points = printer.exclude_object.objects | map(attribute='polygon') | sum(start=[]) %} + {% set purge_x_min = (all_points | map(attribute=0) | min | default(0)) %} + {% set purge_x_max = (all_points | map(attribute=0) | max | default(0)) %} + {% set purge_y_min = (all_points | map(attribute=1) | min | default(0)) %} + {% set purge_y_max = (all_points | map(attribute=1) | max | default(0)) %} + {% set detect_object = purge_x_min + purge_x_max + purge_y_min + purge_y_max %} + {% set purge_x_center = ([((purge_x_max + purge_x_min) / 2) - (purge_amount / 2), 0] | max) %} + {% set purge_y_center = ([((purge_y_max + purge_y_min) / 2) - (purge_amount / 2), 0] | max) %} + {% if (purge_x_center + purge_amount + rapid_move) > bed_x_max %} + {% set purge_x_center = (bed_x_max - (purge_amount + rapid_move)) %} + {% endif %} + {% if (purge_y_center + purge_amount + rapid_move) > bed_y_max %} + {% set purge_y_center = (bed_y_max - (purge_amount + rapid_move)) %} + {% endif %} + {% set purge_x_origin_low = (purge_x_min - purge_margin) %} + {% set purge_x_origin_high = (purge_x_max + purge_margin) %} + {% set purge_y_origin_low = (purge_y_min - purge_margin) %} + {% set purge_y_origin_high = (purge_y_max + purge_margin) %} + {% set purge_move_speed = (flow_rate / 5.0) * 60 | float %} + + {% if cross_section < 5 %} + + RESPOND TYPE=command MSG="[Extruder] max_extrude_cross_section is insufficient for line purge, please set it to 5 or greater. Purge skipped." + + {% else %} + + {% if verbose_enable == True %} + + RESPOND TYPE=command MSG="Moving filament tip {tip_distance}mm" + + {% endif %} + + {% if detect_object == 0 %} + + RESPOND TYPE=command MSG="No object detected! Using classic purge line." + + {% elif purge_y_origin_low > 0 %} + + RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_center}, {purge_y_origin_low} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s." + + {% elif purge_x_origin_low > 0 %} + + RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_origin_low}, {purge_y_center} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s." + + {% elif purge_y_origin_high < bed_y_max %} + + RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_center}, {purge_y_origin_high} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s." + + {% elif purge_x_origin_high < bed_x_max %} + + RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_origin_high}, {purge_y_center} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s." + + {% else %} + + RESPOND TYPE=command MSG="No space for purge line! Using classic purge line." + + {% endif %} + + SAVE_GCODE_STATE NAME=Prepurge_State + + {% if detect_object == 0 %} + + G92 E0 + G1 Z0.1 F600 + M83 + {RETRACT} + SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=5 + M204 S12000 + SET_VELOCITY_LIMIT ACCEL_TO_DECEL=6000 + M220 S100 + M221 S100 + G1 Z2.0 F1200 + G1 X0.1 Y20 Z0.3 F6000.0 + G1 X0.1 Y180.0 Z0.3 F3000.0 E10.0 + G1 X0.4 Y180.0 Z0.3 F3000.0 + G1 X0.4 Y20.0 Z0.3 F3000.0 E10.0 + G1 Y10.0 F3000.0 + G1 Z2.0 F3000.0 + G92 E0 + M82 + G1 F12000 + G21 + + {% elif purge_y_origin_low > 0 %} + + G92 E0 + G0 F{travel_speed} + G90 + G0 X{purge_x_center} Y{purge_y_origin_low} + G0 Z{purge_height} + M83 + G1 E{tip_distance} F{purge_move_speed} + G1 X{purge_x_center + purge_amount} E{purge_amount} F{purge_move_speed} + {RETRACT} + G0 X{purge_x_center + purge_amount + rapid_move} F{travel_speed} + G92 E0 + M82 + G0 Z{purge_height * 2} F{travel_speed} + + {% elif purge_x_origin_low > 0 %} + + G92 E0 + G0 F{travel_speed} + G90 + G0 X{purge_x_origin_low} Y{purge_y_center} + G0 Z{purge_height} + M83 + G1 E{tip_distance} F{purge_move_speed} + G1 Y{purge_y_center + purge_amount} E{purge_amount} F{purge_move_speed} + {RETRACT} + G0 Y{purge_y_center + purge_amount + rapid_move} F{travel_speed} + G92 E0 + M82 + G0 Z{purge_height * 2} F{travel_speed} + + {% elif purge_y_origin_high < bed_y_max %} + + G92 E0 + G0 F{travel_speed} + G90 + G0 X{purge_x_center} Y{purge_y_origin_high} + G0 Z{purge_height} + M83 + G1 E{tip_distance} F{purge_move_speed} + G1 X{purge_x_center + purge_amount} E{purge_amount} F{purge_move_speed} + {RETRACT} + G0 X{purge_x_center + purge_amount + rapid_move} F{travel_speed} + G92 E0 + M82 + G0 Z{purge_height * 2} F{travel_speed} + + {% elif purge_x_origin_high < bed_x_max %} + + G92 E0 + G0 F{travel_speed} + G90 + G0 X{purge_x_origin_high} Y{purge_y_center} + G0 Z{purge_height} + M83 + G1 E{tip_distance} F{purge_move_speed} + G1 Y{purge_y_center + purge_amount} E{purge_amount} F{purge_move_speed} + {RETRACT} + G0 Y{purge_y_center + purge_amount + rapid_move} F{travel_speed} + G92 E0 + M82 + G0 Z{purge_height * 2} F{travel_speed} + + {% else %} + + G92 E0 + G1 Z0.1 F600 + M83 + {RETRACT} + SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=5 + M204 S12000 + SET_VELOCITY_LIMIT ACCEL_TO_DECEL=6000 + M220 S100 + M221 S100 + G1 Z2.0 F1200 + G1 X0.1 Y20 Z0.3 F6000.0 + G1 X0.1 Y180.0 Z0.3 F3000.0 E10.0 + G1 X0.4 Y180.0 Z0.3 F3000.0 + G1 X0.4 Y20.0 Z0.3 F3000.0 E10.0 + G1 Y10.0 F3000.0 + G1 Z2.0 F3000.0 + G92 E0 + M82 + G1 F12000 + G21 + + {% endif %} + + RESTORE_GCODE_STATE NAME=Prepurge_State + + {% endif %} diff --git a/files/kamp/Prusa_Slicer.cfg b/files/kamp/Prusa_Slicer.cfg new file mode 100644 index 0000000..6ed8dc4 --- /dev/null +++ b/files/kamp/Prusa_Slicer.cfg @@ -0,0 +1,32 @@ +########################################### +# PrusaSlicer Macros for Creality K1 Series +########################################### + +[gcode_macro DEFINE_OBJECT] +gcode: + EXCLUDE_OBJECT_DEFINE {rawparams} + + +[gcode_macro START_CURRENT_OBJECT] +gcode: + EXCLUDE_OBJECT_START NAME={params.NAME} + + +[gcode_macro END_CURRENT_OBJECT] +gcode: + EXCLUDE_OBJECT_END {% if params.NAME %}NAME={params.NAME}{% endif %} + + +[gcode_macro LIST_OBJECTS] +gcode: + EXCLUDE_OBJECT_DEFINE + + +[gcode_macro LIST_EXCLUDED_OBJECTS] +gcode: + EXCLUDE_OBJECT + + +[gcode_macro REMOVE_ALL_EXCLUDED] +gcode: + EXCLUDE_OBJECT RESET=1 diff --git a/files/kamp/Smart_Park.cfg b/files/kamp/Smart_Park.cfg new file mode 100644 index 0000000..bf08741 --- /dev/null +++ b/files/kamp/Smart_Park.cfg @@ -0,0 +1,79 @@ +########################################### +# Smart Park for Creality K1 Series +########################################### + +[gcode_macro _SMART_PARK] +description: Parks your printhead near the print area for pre-print hotend heating. +gcode: + {% set kamp_settings = printer["gcode_macro _KAMP_Settings"] %} + {% set bed_x_max = printer["gcode_macro PRINTER_PARAM"].max_x_position | float %} + {% set bed_y_max = printer["gcode_macro PRINTER_PARAM"].max_y_position | float %} + {% set z_height = kamp_settings.smart_park_height | float %} + {% set purge_margin = kamp_settings.purge_margin | float %} + {% set purge_amount = kamp_settings.purge_amount | float %} + {% set verbose_enable = kamp_settings.verbose_enable | abs %} + {% set center_x = bed_x_max / 2 %} + {% set center_y = bed_y_max / 2 %} + {% set axis_minimum_x = printer.toolhead.axis_minimum.x | float %} + {% set axis_minimum_y = printer.toolhead.axis_minimum.y | float %} + {% set all_points = printer.exclude_object.objects | map(attribute='polygon') | sum(start=[]) %} + {% set x_min = (all_points | map(attribute=0) | min | default(0)) %} + {% set x_max = (all_points | map(attribute=0) | max | default(0)) %} + {% set y_min = (all_points | map(attribute=1) | min | default(0)) %} + {% set y_max = (all_points | map(attribute=1) | max | default(0)) %} + {% set travel_speed = (printer.toolhead.max_velocity) * 60 | float %} + {% set rapid_move = 10 %} + {% set park_x_center = ([((x_max + x_min) / 2) - (purge_amount / 2), 0] | max) %} + {% set park_y_center = ([((y_max + y_min) / 2) - (purge_amount / 2), 0] | max) %} + {% if (park_x_center + purge_amount + rapid_move) > bed_x_max %} + {% set park_x_center = (bed_x_max - (purge_amount + rapid_move)) %} + {% endif %} + {% if (park_y_center + purge_amount + rapid_move) > bed_y_max %} + {% set park_y_center = (bed_y_max - (purge_amount + rapid_move)) %} + {% endif %} + {% set park_x_origin_low = (x_min - purge_margin) %} + {% set park_x_origin_high = (x_max + purge_margin) %} + {% set park_y_origin_low = (y_min - purge_margin) %} + {% set park_y_origin_high = (y_max + purge_margin) %} + {% set detect_object = (x_min + x_max + y_min + y_max) %} + {% if detect_object == 0 %} + {% set x_min = 10 %} + {% set y_min = 10 %} + {% set z_height = 2 %} + {% elif park_y_origin_low > 0 %} + {% set x_min = park_x_center %} + {% set y_min = park_y_origin_low %} + {% elif park_x_origin_low > 0 %} + {% set x_min = park_x_origin_low %} + {% set y_min = park_y_center %} + {% elif park_y_origin_high < bed_y_max %} + {% set x_min = park_x_center %} + {% set y_min = park_y_origin_high %} + {% elif park_x_origin_high < bed_x_max %} + {% set x_min = park_x_origin_high %} + {% set y_min = park_y_center %} + {% else %} + {% set x_min = 10 %} + {% set y_min = 10 %} + {% set z_height = 2 %} + {% endif %} + + {% if verbose_enable == True %} + + RESPOND TYPE=command MSG="Smart Park location: {x_min},{y_min}" + + {% endif %} + + SAVE_GCODE_STATE NAME=Presmartpark_State + + G90 + {% if printer.toolhead.position.z < z_height %} + + G0 Z{z_height} + + {% endif %} + + G0 X{x_min} Y{y_min} F{travel_speed} + G0 Z{z_height} + + RESTORE_GCODE_STATE NAME=Presmartpark_State diff --git a/files/kamp/Start_Print.cfg b/files/kamp/Start_Print.cfg new file mode 100644 index 0000000..e08ed2a --- /dev/null +++ b/files/kamp/Start_Print.cfg @@ -0,0 +1,62 @@ +########################################### +# Start Print Macro for Creality K1 Series +########################################### + +[respond] + +[virtual_pins] + +[output_pin KAMP] +pin: virtual_pin:KAMP_pin +value: 1 + +[output_pin BED_LEVELING] +pin: virtual_pin:BED_LEVELING_pin +value: 1 + + +[gcode_macro START_PRINT] +variable_prepare: 0 +gcode: + WAIT_TEMP_END + CLEAR_PAUSE + {% set g28_extruder_temp = printer.custom_macro.g28_ext_temp %} + {% set bed_temp = printer.custom_macro.default_bed_temp %} + {% set extruder_temp = printer.custom_macro.default_extruder_temp %} + {% if 'BED_TEMP' in params|upper and (params.BED_TEMP|float) %} + {% set bed_temp = params.BED_TEMP %} + {% endif %} + {% if 'EXTRUDER_TEMP' in params|upper and (params.EXTRUDER_TEMP|float) %} + {% set extruder_temp = params.EXTRUDER_TEMP %} + {% endif %} + {% if printer['gcode_macro START_PRINT'].prepare|int == 0 %} + PRINT_PREPARE_CLEAR + CX_ROUGH_G28 EXTRUDER_TEMP={extruder_temp} BED_TEMP={bed_temp} + CX_NOZZLE_CLEAR + ACCURATE_G28 + {% if printer.exclude_object.objects != [] and printer['output_pin KAMP'].value == 1 %} + RESPOND TYPE=command MSG="Starting KAMP Bed Mesh..." + BED_MESH_CLEAR + BED_MESH_CALIBRATE PROFILE=kamp + BED_MESH_PROFILE LOAD="kamp" + {% else %} + {% if printer['output_pin BED_LEVELING'].value == 1 %} + RESPOND TYPE=command MSG="Starting Full Bed Mesh..." + CX_PRINT_LEVELING_CALIBRATION + {% endif %} + BED_MESH_PROFILE LOAD="default" + {% endif %} + {% else %} + PRINT_PREPARE_CLEAR + {% endif %} + {% if printer.exclude_object.objects != [] and printer['output_pin KAMP'].value == 1 %} + _SMART_PARK + M109 S{extruder_temp} + M190 S{bed_temp} + RESPOND TYPE=command MSG="Starting KAMP line purge..." + _LINE_PURGE + {% else %} + RESPOND TYPE=command MSG="Starting classic line purge..." + CX_PRINT_DRAW_ONE_LINE + {% endif %} + SET_VELOCITY_LIMIT ACCEL={printer.configfile.settings.printer.max_accel} diff --git a/files/klipper-virtual-pins/virtual_pins.py b/files/klipper-virtual-pins/virtual_pins.py new file mode 100644 index 0000000..ee19520 --- /dev/null +++ b/files/klipper-virtual-pins/virtual_pins.py @@ -0,0 +1,246 @@ +# Virtual Pins support +# +# Copyright (C) 2023 Pedro Lamas +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +class VirtualPins: + def __init__(self, config): + self._printer = config.get_printer() + ppins = self._printer.lookup_object('pins') + ppins.register_chip('virtual_pin', self) + self._pins = {} + self._oid_count = 0 + self._config_callbacks = [] + self._printer.register_event_handler("klippy:connect", + self.handle_connect) + + def handle_connect(self): + for cb in self._config_callbacks: + cb() + + def setup_pin(self, pin_type, pin_params): + ppins = self._printer.lookup_object('pins') + name = pin_params['pin'] + if name in self._pins: + return self._pins[name] + if pin_type == 'digital_out': + pin = DigitalOutVirtualPin(self, pin_params) + elif pin_type == 'pwm': + pin = PwmVirtualPin(self, pin_params) + elif pin_type == 'adc': + pin = AdcVirtualPin(self, pin_params) + elif pin_type == 'endstop': + pin = EndstopVirtualPin(self, pin_params) + else: + raise ppins.error("unable to create virtual pin of type %s" % ( + pin_type,)) + self._pins[name] = pin + return pin + + def create_oid(self): + self._oid_count += 1 + return self._oid_count - 1 + + def register_config_callback(self, cb): + self._config_callbacks.append(cb) + + def add_config_cmd(self, cmd, is_init=False, on_restart=False): + pass + + def get_query_slot(self, oid): + return 0 + + def seconds_to_clock(self, time): + return 0 + + def get_printer(self): + return self._printer + + def register_response(self, cb, msg, oid=None): + pass + + def alloc_command_queue(self): + pass + + def lookup_command(self, msgformat, cq=None): + return VirtualCommand() + + def lookup_query_command(self, msgformat, respformat, oid=None, + cq=None, is_async=False): + return VirtualCommandQuery(respformat, oid) + + def get_enumerations(self): + return {} + + def print_time_to_clock(self, print_time): + return 0 + + def estimated_print_time(self, eventtime): + return 0 + + def register_stepqueue(self, stepqueue): + pass + + def request_move_queue_slot(self): + pass + + def get_status(self, eventtime): + return { + 'pins': { + name : pin.get_status(eventtime) + for name, pin in self._pins.items() + } + } + +class VirtualCommand: + def send(self, data=(), minclock=0, reqclock=0): + pass + + def get_command_tag(self): + pass + +class VirtualCommandQuery: + def __init__(self, respformat, oid): + entries = respformat.split() + self._response = {} + for entry in entries[1:]: + key, _ = entry.split('=') + self._response[key] = oid if key == 'oid' else 1 + + def send(self, data=(), minclock=0, reqclock=0): + return self._response + + def send_with_preface(self, preface_cmd, preface_data=(), data=(), + minclock=0, reqclock=0): + return self._response + +class VirtualPin: + def __init__(self, mcu, pin_params): + self._mcu = mcu + self._name = pin_params['pin'] + self._pullup = pin_params['pullup'] + self._invert = pin_params['invert'] + self._value = self._pullup + printer = self._mcu.get_printer() + self._real_mcu = printer.lookup_object('mcu') + gcode = printer.lookup_object('gcode') + gcode.register_mux_command("SET_VIRTUAL_PIN", "PIN", self._name, + self.cmd_SET_VIRTUAL_PIN, + desc=self.cmd_SET_VIRTUAL_PIN_help) + + cmd_SET_VIRTUAL_PIN_help = "Set the value of an output pin" + def cmd_SET_VIRTUAL_PIN(self, gcmd): + self._value = gcmd.get_float('VALUE', minval=0., maxval=1.) + + def get_mcu(self): + return self._real_mcu + +class DigitalOutVirtualPin(VirtualPin): + def __init__(self, mcu, pin_params): + VirtualPin.__init__(self, mcu, pin_params) + + def setup_max_duration(self, max_duration): + pass + + def setup_start_value(self, start_value, shutdown_value): + self._value = start_value + + def set_digital(self, print_time, value): + self._value = value + + def get_status(self, eventtime): + return { + 'value': self._value, + 'type': 'digital_out' + } + +class PwmVirtualPin(VirtualPin): + def __init__(self, mcu, pin_params): + VirtualPin.__init__(self, mcu, pin_params) + + def setup_max_duration(self, max_duration): + pass + + def setup_start_value(self, start_value, shutdown_value): + self._value = start_value + + def setup_cycle_time(self, cycle_time, hardware_pwm=False): + pass + + def set_pwm(self, print_time, value, cycle_time=None): + self._value = value + + def get_status(self, eventtime): + return { + 'value': self._value, + 'type': 'pwm' + } + +class AdcVirtualPin(VirtualPin): + def __init__(self, mcu, pin_params): + VirtualPin.__init__(self, mcu, pin_params) + self._callback = None + self._min_sample = 0. + self._max_sample = 0. + printer = self._mcu.get_printer() + printer.register_event_handler("klippy:connect", + self.handle_connect) + + def handle_connect(self): + reactor = self._mcu.get_printer().get_reactor() + reactor.register_timer(self._raise_callback, reactor.monotonic() + 2.) + + def setup_adc_callback(self, report_time, callback): + self._callback = callback + + def setup_minmax(self, sample_time, sample_count, + minval=0., maxval=1., range_check_count=0): + + self._min_sample = minval + self._max_sample = maxval + + def _raise_callback(self, eventtime): + range = self._max_sample - self._min_sample + sample_value = (self._value * range) + self._min_sample + self._callback(eventtime, sample_value) + return eventtime + 2. + + def get_status(self, eventtime): + return { + 'value': self._value, + 'type': 'adc' + } + +class EndstopVirtualPin(VirtualPin): + def __init__(self, mcu, pin_params): + VirtualPin.__init__(self, mcu, pin_params) + self._steppers = [] + + def add_stepper(self, stepper): + self._steppers.append(stepper) + + def query_endstop(self, print_time): + return self._value + + def home_start(self, print_time, sample_time, sample_count, rest_time, + triggered=True): + reactor = self._mcu.get_printer().get_reactor() + completion = reactor.completion() + completion.complete(True) + return completion + + def home_wait(self, home_end_time): + return 1 + + def get_steppers(self): + return list(self._steppers) + + def get_status(self, eventtime): + return { + 'value': self._value, + 'type': 'endstop' + } + +def load_config(config): + return VirtualPins(config) diff --git a/files/macros/M600-support.cfg b/files/macros/M600-support.cfg new file mode 100644 index 0000000..b01241b --- /dev/null +++ b/files/macros/M600-support.cfg @@ -0,0 +1,133 @@ +######################################## +# M600 Support +######################################## + +[respond] + +[idle_timeout] +gcode: + RESPOND TYPE=command MSG="Stopping hotend heating..." + M104 S0 +timeout: 99999999 + + +[filament_switch_sensor filament_sensor] +pause_on_runout: true +switch_pin: !PC15 +runout_gcode: + M600 + + +[gcode_macro M600] +description: Filament Change +variable_m600_state: 0 +gcode: + {% set E = printer["gcode_macro PAUSE"].extrude|float %} + {% set y_park = printer.toolhead.axis_minimum.y|float + 5.0 %} + {% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %} + {% set max_z = printer["gcode_macro PRINTER_PARAM"].max_z_position|float %} + {% set act_z = printer.toolhead.position.z|float %} + {% set z_safe = 0.0 %} + {% if act_z < 48.0 %} + {% set z_safe = 50.0 - act_z %} + {% elif act_z < (max_z - 2.0) %} + {% set z_safe = 2.0 %} + {% elif act_z < max_z %} + {% set z_safe = max_z - act_z %} + {% endif %} + {action_respond_info("z_safe = %s"% (z_safe))} + SET_GCODE_VARIABLE MACRO=M600 VARIABLE=m600_state VALUE=1 + SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=hotend_temp VALUE={printer.extruder.target} + RESPOND TYPE=command MSG="Print paused for filament change!" + PAUSE_BASE + G91 + {% if "xyz" in printer.toolhead.homed_axes %} + {% if printer.extruder.can_extrude|lower == 'true' %} + G1 E-1.0 F180 + G1 E-{E} F4000 + {% else %} + RESPOND TYPE=command MSG="Extruder not hot enough!" + {% endif %} + G1 Z{z_safe} F600 + M400 + G90 + G1 X{x_park} Y{y_park} F30000 + {% endif %} + G91 + {% if printer.extruder.can_extrude|lower == 'true' %} + RESPOND TYPE=command MSG="Extracting filament..." + G1 E20 F180 + G1 E-30 F180 + G1 E-50 F2000 + {% else %} + RESPOND TYPE=command MSG="Extruder not hot enough!" + {% endif %} + SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=fan2_speed VALUE={printer['output_pin fan2'].value} + SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=fan0_speed VALUE={printer['output_pin fan0'].value} + M106 P0 S0 + M106 P2 S0 + SET_IDLE_TIMEOUT TIMEOUT=900 + SET_E_MIN_CURRENT + RESPOND TYPE=command MSG="Replace filament at the extruder inlet and click on Resume button!" + + +[gcode_macro RESUME] +description: Resume the current print +rename_existing: RESUME_BASE +gcode: + RESTORE_E_CURRENT + {% if printer['gcode_macro PRINTER_PARAM'].hotend_temp|int != 0 %} + {% if printer['gcode_macro PRINTER_PARAM'].hotend_temp|int > printer.extruder.temperature %} + RESPOND TYPE=command MSG="Starting hotend heating..." + M109 S{printer['gcode_macro PRINTER_PARAM'].hotend_temp|int} + {% else %} + RESPOND TYPE=command MSG="Starting hotend heating..." + M104 S{printer['gcode_macro PRINTER_PARAM'].hotend_temp|int} + {% endif %} + SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=hotend_temp VALUE=0 + {% endif %} + {% if printer['gcode_macro PRINTER_PARAM'].fan2_speed > 0 %} + {% set s_value = (printer['gcode_macro PRINTER_PARAM'].fan2_speed * 255 - printer['gcode_macro PRINTER_PARAM'].fan2_min) * 255 / (255 - printer['gcode_macro PRINTER_PARAM'].fan2_min)|float %} + M106 P2 S{s_value} + {% endif %} + {% set z_resume_move = printer['gcode_macro PRINTER_PARAM'].z_safe_pause|int %} + {% if z_resume_move > 2 %} + {% set z_resume_move = z_resume_move - 2 %} + G91 + G1 Z-{z_resume_move} F600 + M400 + {% endif %} + {% set E = printer["gcode_macro PAUSE"].extrude|float + 1.0 %} + {% if 'VELOCITY' in params|upper %} + {% set get_params = ('VELOCITY=' + params.VELOCITY) %} + {%else %} + {% set get_params = "" %} + {% endif %} + {% if printer["gcode_macro M600"].m600_state == 1 %} + {% if printer.extruder.can_extrude|lower == 'true' %} + RESPOND TYPE=command MSG="Loading and purging filament..." + G91 + G1 E180 F180 + G90 + M400 + {% else %} + RESPOND TYPE=command MSG="Extruder not hot enough!" + {% endif %} + {% if printer['gcode_macro PRINTER_PARAM'].fan0_speed > 0 %} + {% set s_value = (printer['gcode_macro PRINTER_PARAM'].fan0_speed * 255 - printer['gcode_macro PRINTER_PARAM'].fan0_min) * 255 / (255 - printer['gcode_macro PRINTER_PARAM'].fan0_min)|float %} + M106 P0 S{s_value} + {% endif %} + SET_GCODE_VARIABLE MACRO=M600 VARIABLE=m600_state VALUE=0 + SET_IDLE_TIMEOUT TIMEOUT=99999999 + {% else %} + {% if printer.extruder.can_extrude|lower == 'true' %} + G91 + G1 E{E} F2100 + G90 + M400 + {% else %} + RESPOND TYPE=command MSG="Extruder not hot enough!" + {% endif %} + {% endif %} + RESPOND TYPE=command MSG="Restarting print..." + RESUME_BASE {get_params} diff --git a/files/macros/fans-control.cfg b/files/macros/fans-control.cfg new file mode 100644 index 0000000..f0f9960 --- /dev/null +++ b/files/macros/fans-control.cfg @@ -0,0 +1,142 @@ +######################################## +# Fans Control +######################################## + +[respond] + +[duplicate_pin_override] +pins: PC0, PC5, PB2, ADC_TEMPERATURE + + +[temperature_fan chamber_fan] +pin: PC0 +cycle_time: 0.0100 +hardware_pwm: false +max_power: 1 +shutdown_speed: 0 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC5 +min_temp: 0 +max_temp: 70 +control: watermark +max_delta: 2 +target_temp: 35.0 +max_speed: 1.0 +min_speed: 0.0 + + +[temperature_fan mcu_fan] +pin: PB2 +cycle_time: 0.0100 +hardware_pwm: false +max_power: 1 +shutdown_speed: 0 +sensor_type: temperature_mcu +min_temp: 0 +max_temp: 100 +control: watermark +max_delta: 2 +target_temp: 50.0 +max_speed: 1.0 +min_speed: 0.0 + + +[output_pin mcu_fan] +pin: PB2 +pwm: True +cycle_time: 0.0100 +hardware_pwm: false +value: 0.00 +scale: 255 +shutdown_value: 0.0 + + +[gcode_macro M141] +description: Set Chamber Temperature with slicers +gcode: + {% set s = params.S|float %} + SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=chamber_fan TARGET={s} + RESPOND TYPE=command MSG="Chamber target temperature: {s}°C" + + +[gcode_macro M191] +description: Wait for Chamber Temperature to heat up +gcode: + {% set s = params.S|float %} + {% set chamber_temp = printer["temperature_sensor chamber_temp"].temperature|float %} + {% if s > 0 %} + M141 S{s} + {% endif %} + {% if s > chamber_temp and s <= 90 %} + M140 S100 + RESPOND TYPE=command MSG="Waiting for the bed to heat up the chamber..." + TEMPERATURE_WAIT SENSOR="temperature_fan chamber_fan" MINIMUM={s-1} + RESPOND TYPE=command MSG="Chamber target temperature reached: {s}°C" + M140 S{s} + {% endif %} + + +[gcode_macro M106] +gcode: + {% set fans = printer["gcode_macro PRINTER_PARAM"].fans|int %} + {% set fan = 0 %} + {% set value = 0 %} + {% if params.P is defined %} + {% set tmp = params.P|int %} + {% if tmp < fans %} + {% set fan = tmp %} + {% endif %} + {% endif %} + {% if params.S is defined %} + {% set tmp = params.S|float %} + {% else %} + {% set tmp = 255 %} + {% endif %} + {% if tmp > 0 %} + {% if fan == 0 %} + {% set value = (255 - printer["gcode_macro PRINTER_PARAM"].fan0_min) / 255 * tmp %} + {% if printer['gcode_macro Qmode'].flag | int == 1 %} + SET_GCODE_VARIABLE MACRO=Qmode VARIABLE=fan0_value VALUE={printer["gcode_macro PRINTER_PARAM"].fan0_min + value} + {% if value > (255 - printer['gcode_macro PRINTER_PARAM'].fan0_min) / 2 %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan0_min + (255 - printer['gcode_macro PRINTER_PARAM'].fan0_min) / 2 %} + {% else %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan0_min + value %} + {% endif %} + {% else %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan0_min + value %} + {% endif %} + {% endif %} + {% if fan == 1 %} + {% set value = (255 - printer["gcode_macro PRINTER_PARAM"].fan1_min) / 255 * tmp %} + {% if printer['gcode_macro Qmode'].flag | int == 1 %} + SET_GCODE_VARIABLE MACRO=Qmode VARIABLE=fan1_value VALUE={printer["gcode_macro PRINTER_PARAM"].fan1_min + value} + {% if value > (255 - printer['gcode_macro PRINTER_PARAM'].fan1_min) / 2 %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan1_min + (255 - printer['gcode_macro PRINTER_PARAM'].fan1_min) / 2 %} + {% else %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan1_min + value %} + {% endif %} + {% else %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan1_min + value %} + {% endif %} + {% endif %} + {% if fan == 2 %} + {% set value = (255 - printer["gcode_macro PRINTER_PARAM"].fan2_min) / 255 * tmp %} + {% if printer['gcode_macro Qmode'].flag | int == 1 %} + SET_GCODE_VARIABLE MACRO=Qmode VARIABLE=fan2_value VALUE={printer["gcode_macro PRINTER_PARAM"].fan2_min + value} + {% if value > (255 - printer['gcode_macro PRINTER_PARAM'].fan2_min) / 2 %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan2_min + (255 - printer['gcode_macro PRINTER_PARAM'].fan2_min) / 2 %} + {% else %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan2_min + value %} + {% endif %} + {% else %} + {% set value = printer["gcode_macro PRINTER_PARAM"].fan2_min + value %} + {% endif %} + {% endif %} + {% endif %} + {% if value >= 255 %} + {% set value = 255 %} + {% endif %} + {% if params.P is defined and params.P|int == 3 %} + {% set fan = 1 %} + {% endif %} + SET_PIN PIN=fan{fan} VALUE={value} diff --git a/files/macros/save-zoffset.cfg b/files/macros/save-zoffset.cfg new file mode 100644 index 0000000..abd3002 --- /dev/null +++ b/files/macros/save-zoffset.cfg @@ -0,0 +1,38 @@ +######################################## +# Save Z-Offset +######################################## + +[save_variables] +filename: /usr/data/printer_data/config/Helper-Script/variables.cfg + +[respond] + + +[gcode_macro SET_GCODE_OFFSET] +description: Saving Z-Offset +rename_existing: _SET_GCODE_OFFSET +gcode: + {% if printer.save_variables.variables.zoffset %} + {% set zoffset = printer.save_variables.variables.zoffset %} + {% else %} + {% set zoffset = {'z': None} %} + {% endif %} + {% set ns = namespace(zoffset={'z': zoffset.z}) %} + _SET_GCODE_OFFSET {% for p in params %}{'%s=%s '% (p, params[p])}{% endfor %} + {%if 'Z' in params %}{% set null = ns.zoffset.update({'z': params.Z}) %}{% endif %} + {%if 'Z_ADJUST' in params %} + {%if ns.zoffset.z == None %}{% set null = ns.zoffset.update({'z': 0}) %}{% endif %} + {% set null = ns.zoffset.update({'z': (ns.zoffset.z | float) + (params.Z_ADJUST | float)}) %} + {% endif %} + SAVE_VARIABLE VARIABLE=zoffset VALUE="{ns.zoffset}" + + +[delayed_gcode LOAD_GCODE_OFFSETS] +initial_duration: 2 +gcode: + {% if printer.save_variables.variables.zoffset %} + {% set zoffset = printer.save_variables.variables.zoffset %} + _SET_GCODE_OFFSET {% for axis, offset in zoffset.items() if zoffset[axis] %}{ "%s=%s " % (axis, offset) }{% endfor %} + RESPOND TYPE=command MSG="Loaded Z-Offset from variables.cfg: {zoffset.z}mm" + {% endif %} + \ No newline at end of file diff --git a/files/macros/useful-macros.cfg b/files/macros/useful-macros.cfg new file mode 100644 index 0000000..2a5a033 --- /dev/null +++ b/files/macros/useful-macros.cfg @@ -0,0 +1,205 @@ +######################################## +# Useful Macros +######################################## + +[gcode_shell_command Klipper_Backup] +command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -backup_klipper +timeout: 600.0 +verbose: true + +[gcode_shell_command Klipper_Restore] +command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -restore_klipper +timeout: 600.0 +verbose: true + +[gcode_shell_command Moonraker_Backup] +command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -backup_moonraker +timeout: 600.0 +verbose: true + +[gcode_shell_command Moonraker_Restore] +command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -restore_moonraker +timeout: 600.0 +verbose: true + + +[gcode_macro KLIPPER_BACKUP_CONFIG] +gcode: + RUN_SHELL_COMMAND CMD=Klipper_Backup + + +[gcode_macro KLIPPER_RESTORE_CONFIG] +gcode: + RUN_SHELL_COMMAND CMD=Klipper_Restore + + +[gcode_macro MOONRAKER_BACKUP_DATABASE] +gcode: + RUN_SHELL_COMMAND CMD=Moonraker_Backup + + +[gcode_macro MOONRAKER_RESTORE_DATABASE] +gcode: + RUN_SHELL_COMMAND CMD=Moonraker_Restore + + +[gcode_macro BED_LEVELING] +description: Start Bed Leveling +gcode: + {% if 'PROBE_COUNT' in params|upper %} + {% set get_count = ('PROBE_COUNT=' + params.PROBE_COUNT) %} + {%else %} + {% set get_count = "" %} + {% endif %} + {% set bed_temp = params.BED_TEMP|default(50)|float %} + {% set hotend_temp = params.HOTEND_TEMP|default(140)|float %} + {% set nozzle_clear_temp = params.NOZZLE_CLEAR_TEMP|default(240)|float %} + SET_FILAMENT_SENSOR SENSOR=filament_sensor ENABLE=0 + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + BED_MESH_CLEAR + NOZZLE_CLEAR HOT_MIN_TEMP={hotend_temp} HOT_MAX_TEMP={nozzle_clear_temp} BED_MAX_TEMP={bed_temp} + ACCURATE_G28 + M204 S5000 + SET_VELOCITY_LIMIT ACCEL_TO_DECEL=5000 + BED_MESH_CALIBRATE {get_count} + BED_MESH_OUTPUT + {% set y_park = printer.toolhead.axis_maximum.y/2 %} + {% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %} + G1 X{x_park} Y{y_park} F20000 + TURN_OFF_HEATERS + SET_FILAMENT_SENSOR SENSOR=filament_sensor ENABLE=1 + + +[gcode_macro PID_BED] +description: Start Bed PID +gcode: + G90 + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + G1 Z10 F600 + M106 + PID_CALIBRATE HEATER=heater_bed TARGET={params.BED_TEMP|default(70)} + M107 + {% set y_park = printer.toolhead.axis_maximum.y/2 %} + {% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %} + G1 X{x_park} Y{y_park} F20000 + + +[gcode_macro PID_HOTEND] +description: Start Hotend PID +gcode: + G90 + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + G1 Z10 F600 + M106 + PID_CALIBRATE HEATER=extruder TARGET={params.HOTEND_TEMP|default(250)} + M107 + {% set y_park = printer.toolhead.axis_maximum.y/2 %} + {% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %} + G1 X{x_park} Y{y_park} F20000 + WAIT_TEMP_START + + +[gcode_macro LUBRICATE_RODS] +description: Distribute lubricant on Rods +gcode: + {% set min_speed = 3000 %} # Minimum speed in mm/min + {% set max_speed = 18000 %} # Maximum speed in mm/min + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + G1 Z50 F300 + {% set x_max = printer.toolhead.axis_maximum.x|int %} + {% set y_max = printer.toolhead.axis_maximum.y|int %} + {% set edge_offset_x = x_max * 0.05 %} + {% set edge_offset_y = y_max * 0.05 %} + {% set x_range = x_max - edge_offset_x %} + {% set y_range = y_max - edge_offset_y %} + {% set num_steps_x = (x_range / 10)|int %} + {% set num_steps_y = (y_range / 10)|int %} + {% set speed_increment_x = (max_speed - min_speed) / num_steps_x %} + {% set speed_increment_y = (max_speed - min_speed) / num_steps_y %} + {% set current_speed_x = min_speed %} + {% set current_speed_y = min_speed %} + {% for i in range(num_steps_x) %} + G1 X{edge_offset_x + i * 10} Y{edge_offset_y} F{current_speed_x} + G1 X{edge_offset_x + i * 10} Y{y_range} F{current_speed_x} + {% set current_speed_x = current_speed_x + speed_increment_x %} + {% endfor %} + {% for j in range(num_steps_y) %} + G1 Y{edge_offset_y + j * 10} X{edge_offset_x} F{current_speed_y} + G1 Y{edge_offset_y + j * 10} X{x_range} F{current_speed_y} + {% set current_speed_y = current_speed_y + speed_increment_y %} + {% endfor %} + +[gcode_macro WARMUP] +description: Stress Test +variable_maxd: 14142.14 ; = SQRT(2*maxy) +gcode: + {% set min_loops = 2 %} + {% set max_loops = params.LOOPS|default(3)|int %} + {% if 'LOOPS' in params|upper %} + {% if max_loops < min_loops %} + {% set max_loops = min_loops %} + {% endif %} + {% endif %} + {% set loop_cnt = max_loops %} + {% if 'X_ACCEL_MAX' in params|upper %} + {% set maxx = params.X_ACCEL_MAX|default(10000)|int %} + {% endif %} + {% if 'Y_ACCEL_MAX' in params|upper %} + {% set maxy = params.Y_ACCEL_MAX|default(10000)|int %} + {% endif %} + {% set max_x = (printer.toolhead.axis_maximum.x|int-5) %} + {% set max_y = (printer.toolhead.axis_maximum.y|int-5) %} + {% set loop_step_y = max_y//(loop_cnt-1) %} + {% set loop_step_x = max_x//(loop_cnt-1) %} + {% set y_park = printer.toolhead.axis_maximum.y/2 %} + {% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %} + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + SET_VELOCITY_LIMIT ACCEL={maxx} ACCEL_TO_DECEL={maxx/2} + {% for number in range(10,max_y+11,loop_step_y) %} + {% if number >= max_y %} + {% set number = max_y %} + {% endif %} + G1 F{maxy} X10 Y{number} + G1 F{maxx} X{max_x} Y{number} + {% endfor %} + SET_VELOCITY_LIMIT ACCEL={maxy} ACCEL_TO_DECEL={maxy/2} + {% for number in range(10,max_x+11,loop_step_y) %} + {% if number >= max_x %} + {% set number = max_x %} + {% endif %} + G1 F{maxy} X{number} Y{max_y} + G1 F{maxy} X{number} Y10 + {% endfor %} + SET_VELOCITY_LIMIT ACCEL={maxd} ACCEL_TO_DECEL={maxd/2} + {% for times in range(loop_cnt) %} + G1 F{maxx} X10 Y10 + G1 F{maxd} X{max_x} Y{max_y} + G1 F{maxx} X10 Y{max_y} + G1 F{maxd} X{max_x} Y10 + G1 F{maxy} X{max_x} Y{max_y} + G1 F{maxd} X10 Y10 + G1 F{maxy} X10 Y{max_y} + G1 F{maxd} X{max_x} Y10 + {% endfor %} + SET_VELOCITY_LIMIT ACCEL={maxx} ACCEL_TO_DECEL={maxx/2} + {% for times in range(loop_cnt) %} + G1 F{maxy} X10 Y10 + G1 F{maxy} X10 Y{max_y} + G1 F{maxx} X{max_x} Y{max_y} + G1 F{maxy} X{max_x} Y10 + G1 F{maxx} X10 Y10 + G1 F{maxx} X{max_x} Y10 + G1 F{maxy} X{max_x} Y{max_y} + G1 F{maxx} X10 Y{max_y} + {% endfor %} + G1 X{x_park} Y{y_park} F30000 diff --git a/files/moonraker-timelapse/timelapse.cfg b/files/moonraker-timelapse/timelapse.cfg new file mode 100644 index 0000000..c821f39 --- /dev/null +++ b/files/moonraker-timelapse/timelapse.cfg @@ -0,0 +1,427 @@ +# Timelapse klipper macro definition +# +# Copyright (C) 2021 Christoph Frei +# Copyright (C) 2021 Alex Zellner +# +# This file may be distributed under the terms of the GNU GPLv3 license +# +# Macro version 1.15 +# + +##### DO NOT CHANGE ANY MACRO!!! ##### + +########################################################################## +# # +# GET_TIMELAPSE_SETUP: Print the Timelapse setup to console # +# # +########################################################################## + +[gcode_macro GET_TIMELAPSE_SETUP] +description: Print the Timelapse setup +gcode: + {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %} + {% set output_txt = ["Timelapse Setup:"] %} + {% set _dummy = output_txt.append("enable: %s" % tl.enable) %} + {% set _dummy = output_txt.append("park: %s" % tl.park.enable) %} + {% if tl.park.enable %} + {% set _dummy = output_txt.append("park position: %s time: %s s" % (tl.park.pos, tl.park.time)) %} + {% set _dummy = output_txt.append("park cord x:%s y:%s dz:%s" % (tl.park.coord.x, tl.park.coord.y, tl.park.coord.dz)) %} + {% set _dummy = output_txt.append("travel speed: %s mm/s" % tl.speed.travel) %} + {% endif %} + {% set _dummy = output_txt.append("fw_retract: %s" % tl.extruder.fw_retract) %} + {% if not tl.extruder.fw_retract %} + {% set _dummy = output_txt.append("retract: %s mm speed: %s mm/s" % (tl.extruder.retract, tl.speed.retract)) %} + {% set _dummy = output_txt.append("extrude: %s mm speed: %s mm/s" % (tl.extruder.extrude, tl.speed.extrude)) %} + {% endif %} + {% set _dummy = output_txt.append("verbose: %s" % tl.verbose) %} + {action_respond_info(output_txt|join("\n"))} + +################################################################################################ +# # +# Use _SET_TIMELAPSE_SETUP [ENABLE=value] [VERBOSE=value] [PARK_ENABLE=value] [PARK_POS=value] # +# [PARK_TIME=value] [CUSTOM_POS_X=value] [CUSTOM_POS_Y=value] # +# [CUSTOM_POS_DZ=value][TRAVEL_SPEED=value] [RETRACT_SPEED=value] # +# [EXTRUDE_SPEED=value] [EXTRUDE_DISTANCE=value] # +# [RETRACT_DISTANCE=value] [FW_RETRACT=value] # +# # +################################################################################################ + +[gcode_macro _SET_TIMELAPSE_SETUP] +description: Set user parameters for timelapse +gcode: + {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %} + ##### get min and max bed size ##### + {% set min = printer.toolhead.axis_minimum %} + {% set max = printer.toolhead.axis_maximum %} + {% set round_bed = True if printer.configfile.settings.printer.kinematics is in ['delta','polar','rotary_delta','winch'] + else False %} + {% set park = {'min' : {'x': (min.x / 1.42)|round(3) if round_bed else min.x|round(3), + 'y': (min.y / 1.42)|round(3) if round_bed else min.y|round(3)}, + 'max' : {'x': (max.x / 1.42)|round(3) if round_bed else max.x|round(3), + 'y': (max.y / 1.42)|round(3) if round_bed else max.y|round(3)}, + 'center': {'x': (max.x-(max.x-min.x)/2)|round(3), + 'y': (max.y-(max.y-min.y)/2)|round(3)}} %} + ##### set new values ##### + {% if params.ENABLE %} + {% if params.ENABLE|lower is in ['true', 'false'] %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=enable VALUE={True if params.ENABLE|lower == 'true' else False} + {% else %} + {action_raise_error("ENABLE=%s not supported. Allowed values are [True, False]" % params.ENABLE|capitalize)} + {% endif %} + {% endif %} + {% if params.VERBOSE %} + {% if params.VERBOSE|lower is in ['true', 'false'] %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=verbose VALUE={True if params.VERBOSE|lower == 'true' else False} + {% else %} + {action_raise_error("VERBOSE=%s not supported. Allowed values are [True, False]" % params.VERBOSE|capitalize)} + {% endif %} + {% endif %} + {% if params.CUSTOM_POS_X %} + {% if params.CUSTOM_POS_X|float >= min.x and params.CUSTOM_POS_X|float <= max.x %} + {% set _dummy = tl.park.custom.update({'x':params.CUSTOM_POS_X|float|round(3)}) %} + {% else %} + {action_raise_error("CUSTOM_POS_X=%s must be within [%s - %s]" % (params.CUSTOM_POS_X, min.x, max.x))} + {% endif %} + {% endif %} + {% if params.CUSTOM_POS_Y %} + {% if params.CUSTOM_POS_Y|float >= min.y and params.CUSTOM_POS_Y|float <= max.y %} + {% set _dummy = tl.park.custom.update({'y':params.CUSTOM_POS_Y|float|round(3)}) %} + {% else %} + {action_raise_error("CUSTOM_POS_Y=%s must be within [%s - %s]" % (params.CUSTOM_POS_Y, min.y, max.y))} + {% endif %} + {% endif %} + {% if params.CUSTOM_POS_DZ %} + {% if params.CUSTOM_POS_DZ|float >= min.z and params.CUSTOM_POS_DZ|float <= max.z %} + {% set _dummy = tl.park.custom.update({'dz':params.CUSTOM_POS_DZ|float|round(3)}) %} + {% else %} + {action_raise_error("CUSTOM_POS_DZ=%s must be within [%s - %s]" % (params.CUSTOM_POS_DZ, min.z, max.z))} + {% endif %} + {% endif %} + {% if params.PARK_ENABLE %} + {% if params.PARK_ENABLE|lower is in ['true', 'false'] %} + {% set _dummy = tl.park.update({'enable':True if params.PARK_ENABLE|lower == 'true' else False}) %} + {% else %} + {action_raise_error("PARK_ENABLE=%s not supported. Allowed values are [True, False]" % params.PARK_ENABLE|capitalize)} + {% endif %} + {% endif %} + {% if params.PARK_POS %} + {% if params.PARK_POS|lower is in ['center','front_left','front_right','back_left','back_right','custom','x_only','y_only'] %} + {% set dic = {'center' : {'x': park.center.x , 'y': park.center.y , 'dz': 1 }, + 'front_left' : {'x': park.min.x , 'y': park.min.y , 'dz': 0 }, + 'front_right' : {'x': park.max.x , 'y': park.min.y , 'dz': 0 }, + 'back_left' : {'x': park.min.x , 'y': park.max.y , 'dz': 0 }, + 'back_right' : {'x': park.max.x , 'y': park.max.y , 'dz': 0 }, + 'custom' : {'x': tl.park.custom.x, 'y': tl.park.custom.y, 'dz': tl.park.custom.dz}, + 'x_only' : {'x': tl.park.custom.x, 'y': 'none' , 'dz': tl.park.custom.dz}, + 'y_only' : {'x': 'none' , 'y': tl.park.custom.y, 'dz': tl.park.custom.dz}} %} + {% set _dummy = tl.park.update({'pos':params.PARK_POS|lower}) %} + {% set _dummy = tl.park.update({'coord':dic[tl.park.pos]}) %} + {% else %} + {action_raise_error("PARK_POS=%s not supported. Allowed values are [CENTER, FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT, CUSTOM, X_ONLY, Y_ONLY]" + % params.PARK_POS|upper)} + {% endif %} + {% endif %} + {% if params.PARK_TIME %} + {% if params.PARK_TIME|float >= 0.0 %} + {% set _dummy = tl.park.update({'time':params.PARK_TIME|float|round(3)}) %} + {% else %} + {action_raise_error("PARK_TIME=%s must be a positive number" % params.PARK_TIME)} + {% endif %} + {% endif %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=park VALUE="{tl.park}" + {% if params.TRAVEL_SPEED %} + {% if params.TRAVEL_SPEED|float > 0.0 %} + {% set _dummy = tl.speed.update({'travel':params.TRAVEL_SPEED|float|round(3)}) %} + {% else %} + {action_raise_error("TRAVEL_SPEED=%s must be larger than 0" % params.TRAVEL_SPEED)} + {% endif %} + {% endif %} + {% if params.RETRACT_SPEED %} + {% if params.RETRACT_SPEED|float > 0.0 %} + {% set _dummy = tl.speed.update({'retract':params.RETRACT_SPEED|float|round(3)}) %} + {% else %} + {action_raise_error("RETRACT_SPEED=%s must be larger than 0" % params.RETRACT_SPEED)} + {% endif %} + {% endif %} + {% if params.EXTRUDE_SPEED %} + {% if params.EXTRUDE_SPEED|float > 0.0 %} + {% set _dummy = tl.speed.update({'extrude':params.EXTRUDE_SPEED|float|round(3)}) %} + {% else %} + {action_raise_error("EXTRUDE_SPEED=%s must be larger than 0" % params.EXTRUDE_SPEED)} + {% endif %} + {% endif %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=speed VALUE="{tl.speed}" + {% if params.EXTRUDE_DISTANCE %} + {% if params.EXTRUDE_DISTANCE|float >= 0.0 %} + {% set _dummy = tl.extruder.update({'extrude':params.EXTRUDE_DISTANCE|float|round(3)}) %} + {% else %} + {action_raise_error("EXTRUDE_DISTANCE=%s must be specified as positiv number" % params.EXTRUDE_DISTANCE)} + {% endif %} + {% endif %} + {% if params.RETRACT_DISTANCE %} + {% if params.RETRACT_DISTANCE|float >= 0.0 %} + {% set _dummy = tl.extruder.update({'retract':params.RETRACT_DISTANCE|float|round(3)}) %} + {% else %} + {action_raise_error("RETRACT_DISTANCE=%s must be specified as positiv number" % params.RETRACT_DISTANCE)} + {% endif %} + {% endif %} + {% if params.FW_RETRACT %} + {% if params.FW_RETRACT|lower is in ['true', 'false'] %} + {% if 'firmware_retraction' in printer.configfile.settings %} + {% set _dummy = tl.extruder.update({'fw_retract': True if params.FW_RETRACT|lower == 'true' else False}) %} + {% else %} + {% set _dummy = tl.extruder.update({'fw_retract':False}) %} + {% if params.FW_RETRACT|capitalize == 'True' %} + {action_raise_error("[firmware_retraction] not defined in printer.cfg. Can not enable fw_retract")} + {% endif %} + {% endif %} + {% else %} + {action_raise_error("FW_RETRACT=%s not supported. Allowed values are [True, False]" % params.FW_RETRACT|capitalize)} + {% endif %} + {% endif %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=extruder VALUE="{tl.extruder}" + {% if printer.configfile.settings['gcode_macro pause'] is defined %} + {% set _dummy = tl.macro.update({'pause': printer.configfile.settings['gcode_macro pause'].rename_existing}) %} + {% endif %} + {% if printer.configfile.settings['gcode_macro resume'] is defined %} + {% set _dummy = tl.macro.update({'resume': printer.configfile.settings['gcode_macro resume'].rename_existing}) %} + {% endif %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=macro VALUE="{tl.macro}" + +########################################################################## +# # +# TIMELAPSE_TAKE_FRAME: take the next picture # +# # +########################################################################## + +######################### definition ######################### +## enable: enable or disable the next frame. Valid inputs: [True, False] +## takingframe: internal use. Valid inputs: [True, False] +## +## park.enable: enable or disable to park the head while taking a picture. Valid inputs: [True, False] +## park.pos : used position for parking. Valid inputs: [center, front_left, front_right, back_left, back_right, custom, x_only, y_only] +## park.time : used for the debug macro. Time in s +## park.custom.x, park.custom.y: coordinates of the custom parkposition. Unit [mm] +## park.custom.dz : custom z hop for the picture. Unit [mm] +## park.coord : internal use +## +## extruder.fw_retract: enable disable fw retraction [True,False] +## extruder.extrude : filament extruded at the end of park. Unit [mm] +## extruder.retract : filament retract at the start of park. Unit [mm] +## +## speed.travel : used speed for travel from and to the park positon. Unit: [mm/min] +## speed.retract: used speed for retract [mm/min] +## speed.extrude: used speed for extrude [mm/min] +## +## verbose: Enable mesage output of TIMELAPSE_TAKE_FRAME +## +## check_time: time when the status of the taken picture is checked. Default 0.5 sec +## +## restore.absolute.coordinates: internal use +## restore.absolute.extrude : internal use +## restore.speed : internal use +## restore.e : internal use +## restore.factor.speed : internal use +## restore.factor.extrude : internal use +## +## macro.pause : internal use +## macro.resume : internal use +## +## is_paused: internal use +############################################################### +[gcode_macro TIMELAPSE_TAKE_FRAME] +description: Take Timelapse shoot +variable_enable: False +variable_takingframe: False +variable_park: {'enable': False, + 'pos' : 'center', + 'time' : 0.1, + 'custom': {'x': 0, 'y': 0, 'dz': 0}, + 'coord' : {'x': 0, 'y': 0, 'dz': 0}} +variable_extruder: {'fw_retract': False, + 'retract': 1.0, + 'extrude': 1.0} +variable_speed: {'travel': 100, + 'retract': 15, + 'extrude': 15} +variable_verbose: True +variable_check_time: 0.5 +variable_restore: {'absolute': {'coordinates': True, 'extrude': True}, 'speed': 1500, 'e':0, 'factor': {'speed': 1.0, 'extrude': 1.0}} +variable_macro: {'pause': 'PAUSE', 'resume': 'RESUME'} +variable_is_paused: False +gcode: + {% set hyperlapse = True if params.HYPERLAPSE and params.HYPERLAPSE|lower =='true' else False %} + {% if enable %} + {% if (hyperlapse and printer['gcode_macro HYPERLAPSE'].run) or + (not hyperlapse and not printer['gcode_macro HYPERLAPSE'].run) %} + {% if park.enable %} + {% set pos = {'x': 'X' + park.coord.x|string if park.pos != 'y_only' else '', + 'y': 'Y' + park.coord.y|string if park.pos != 'x_only' else '', + 'z': 'Z'+ [printer.gcode_move.gcode_position.z + park.coord.dz, printer.toolhead.axis_maximum.z]|min|string} %} + {% set restore = {'absolute': {'coordinates': printer.gcode_move.absolute_coordinates, + 'extrude' : printer.gcode_move.absolute_extrude}, + 'speed' : printer.gcode_move.speed, + 'e' : printer.gcode_move.gcode_position.e, + 'factor' : {'speed' : printer.gcode_move.speed_factor, + 'extrude': printer.gcode_move.extrude_factor}} %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=restore VALUE="{restore}" + {% if not printer[printer.toolhead.extruder].can_extrude %} + {% if verbose %}{action_respond_info("Timelapse: Warning, minimum extruder temperature not reached!")}{% endif %} + {% else %} + {% if extruder.fw_retract %} + G10 + {% else %} + M83 ; insure relative extrusion + G0 E-{extruder.retract} F{speed.retract * 60} + {% endif %} + {% endif %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=is_paused VALUE=True + {macro.pause} ; execute the klipper PAUSE command + SET_GCODE_OFFSET X=0 Y=0 ; this will insure that the head parks always at the same position in a multi setup + G90 ; insure absolute move + {% if "xyz" not in printer.toolhead.homed_axes %} + {% if verbose %}{action_respond_info("Timelapse: Warning, axis not homed yet!")}{% endif %} + {% else %} + G0 {pos.x} {pos.y} {pos.z} F{speed.travel * 60} + {% endif %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=takingframe VALUE=True + UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_TAKE_FRAME DURATION={check_time} + M400 + {% endif %} + _TIMELAPSE_NEW_FRAME HYPERLAPSE={hyperlapse} + {% endif %} + {% else %} + {% if verbose %}{action_respond_info("Timelapse: disabled, take frame ignored")}{% endif %} + {% endif %} + +[gcode_macro _TIMELAPSE_NEW_FRAME] +description: action call for timelapse shoot. must be a seperate macro +gcode: + {action_call_remote_method("timelapse_newframe", + macropark=printer['gcode_macro TIMELAPSE_TAKE_FRAME'].park, + hyperlapse=params.HYPERLAPSE)} + +[delayed_gcode _WAIT_TIMELAPSE_TAKE_FRAME] +gcode: + {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %} + {% set factor = {'speed': printer.gcode_move.speed_factor, 'extrude': printer.gcode_move.extrude_factor} %} + {% if tl.takingframe %} + UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_TAKE_FRAME DURATION={tl.check_time} + {% else %} + {tl.macro.resume} VELOCITY={tl.speed.travel} ; execute the klipper RESUME command + SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=is_paused VALUE=False + {% if not printer[printer.toolhead.extruder].can_extrude %} + {action_respond_info("Timelapse: Warning minimum extruder temperature not reached!")} + {% else %} + {% if tl.extruder.fw_retract %} + G11 + {% else %} + G0 E{tl.extruder.extrude} F{tl.speed.extrude * 60} + G0 F{tl.restore.speed} + {% if tl.restore.absolute.extrude %} + M82 + G92 E{tl.restore.e} + {% endif %} + {% endif %} + {% endif %} + {% if tl.restore.factor.speed != factor.speed %} M220 S{(factor.speed*100)|round(0)} {% endif %} + {% if tl.restore.factor.extrude != factor.extrude %} M221 S{(factor.extrude*100)|round(0)} {% endif %} + {% if not tl.restore.absolute.coordinates %} G91 {% endif %} + {% endif %} + +#################################################################################################### +# # +# HYPERLAPSE: Starts or stops a Hyperlapse video # +# Usage: HYPERLAPSE ACTION=START [CYCLE=time] starts a hyperlapse with cycle time (default 30 sec) # +# HYPERLAPSE ACTION=STOP stops the hyperlapse recording # +# # +#################################################################################################### + +######################### definition ######################### +## cycle: cycle time in seconds +## run: internal use [True/False] +############################################################### +[gcode_macro HYPERLAPSE] +description: Start/Stop a hyperlapse recording +variable_cycle: 0 +variable_run: False +gcode: + {% set cycle = params.CYCLE|default(30)|int %} + {% if params.ACTION and params.ACTION|lower == 'start' %} + {action_respond_info("Hyperlapse: frames started (Cycle %d sec)" % cycle)} + SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=run VALUE=True + SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=cycle VALUE={cycle} + UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION={cycle} + TIMELAPSE_TAKE_FRAME HYPERLAPSE=True + {% elif params.ACTION and params.ACTION|lower == 'stop' %} + {% if run %}{action_respond_info("Hyperlapse: frames stopped")}{% endif %} + SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=run VALUE=False + UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION=0 + {% else %} + {action_raise_error("Hyperlapse: No valid input parameter + Use: + - HYPERLAPSE ACTION=START [CYCLE=time] + - HYPERLAPSE ACTION=STOP")} + {% endif %} + +[delayed_gcode _HYPERLAPSE_LOOP] +gcode: + UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION={printer["gcode_macro HYPERLAPSE"].cycle} + TIMELAPSE_TAKE_FRAME HYPERLAPSE=True + +########################################################################## +# # +# TIMELAPSE_RENDER: Render the video at print end # +# # +########################################################################## + +######################### definition ######################### +## render: internal use. Valid inputs: [True, False] +## run_identifier: internal use. Valid input [0 .. 3] +############################################################### +[gcode_macro TIMELAPSE_RENDER] +description: Render Timelapse video and wait for the result +variable_render: False +variable_run_identifier: 0 +gcode: + {action_respond_info("Timelapse: Rendering started")} + {action_call_remote_method("timelapse_render", byrendermacro="True")} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_RENDER VARIABLE=render VALUE=True + {printer.configfile.settings['gcode_macro pause'].rename_existing} ; execute the klipper PAUSE command + UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_RENDER DURATION=0.5 + +[delayed_gcode _WAIT_TIMELAPSE_RENDER] +gcode: + {% set ri = printer['gcode_macro TIMELAPSE_RENDER'].run_identifier % 4 %} + SET_GCODE_VARIABLE MACRO=TIMELAPSE_RENDER VARIABLE=run_identifier VALUE={ri + 1} + {% if printer['gcode_macro TIMELAPSE_RENDER'].render %} + M117 Rendering {['-','\\','|','/'][ri]} + UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_RENDER DURATION=0.5 + {% else %} + {action_respond_info("Timelapse: Rendering finished")} + M117 + {printer.configfile.settings['gcode_macro resume'].rename_existing} ; execute the klipper RESUME command + {% endif %} + +########################################################################## +# # +# TEST_STREAM_DELAY: Helper macro to find stream and park delay # +# # +########################################################################## + +[gcode_macro TEST_STREAM_DELAY] +description: Helper macro to find stream and park delay +gcode: + {% set min = printer.toolhead.axis_minimum %} + {% set max = printer.toolhead.axis_maximum %} + {% set act = printer.toolhead.position %} + {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %} + {% if act.z > 5.0 %} + G0 X{min.x + 5.0} F{tl.speed.travel|int * 60} + G0 X{(max.x-min.x)/2} + G4 P{tl.park.time|float * 1000} + _TIMELAPSE_NEW_FRAME HYPERLAPSE=FALSE + G0 X{max.x - 5.0} + {% else %} + {action_raise_error("Toolhead z %.3f to low. Please place head above z = 5.0" % act.z)} + {% endif %} diff --git a/files/moonraker-timelapse/timelapse.py b/files/moonraker-timelapse/timelapse.py new file mode 100644 index 0000000..8ce271f --- /dev/null +++ b/files/moonraker-timelapse/timelapse.py @@ -0,0 +1,842 @@ +# Moonraker Timelapse component for K1 Series +# +# Copyright (C) 2021 Christoph Frei +# +# This file may be distributed under the terms of the GNU GPLv3 license. +from __future__ import annotations +import logging +import os +import glob +import re +import shutil +import asyncio +from datetime import datetime +from tornado.ioloop import IOLoop +from zipfile import ZipFile + +# Annotation imports +from typing import ( + TYPE_CHECKING, + Dict, + Any +) +if TYPE_CHECKING: + from confighelper import ConfigHelper + from .webcam import WebcamManager, WebCam + from websockets import WebRequest + from . import shell_command + from . import klippy_apis + from . import database + + APIComp = klippy_apis.KlippyAPI + SCMDComp = shell_command.ShellCommandFactory + DBComp = database.MoonrakerDatabase + + +class Timelapse: + + def __init__(self, confighelper: ConfigHelper) -> None: + + # setup vars + self.renderisrunning = False + self.saveisrunning = False + self.takingframe = False + self.framecount = 0 + self.lastframefile = "" + self.lastrenderprogress = 0 + self.lastcmdreponse = "" + self.byrendermacro = False + self.hyperlapserunning = False + self.printing = False + self.noWebcamDb = False + + self.confighelper = confighelper + self.server = confighelper.get_server() + self.klippy_apis: APIComp = self.server.lookup_component('klippy_apis') + self.database: DBComp = self.server.lookup_component("database") + + # setup static (nonDB) settings + out_dir_cfg = confighelper.get( + "output_path", "~/timelapse/") + temp_dir_cfg = confighelper.get( + "frame_path", "/tmp/timelapse/") + self.ffmpeg_binary_path = confighelper.get( + "ffmpeg_binary_path", "/opt/bin/ffmpeg") + self.wget_skip_cert = confighelper.getboolean( + "wget_skip_cert_check", False) + + # Setup default config + self.config: Dict[str, Any] = { + 'enabled': True, + 'mode': "layermacro", + 'camera': "", + 'snapshoturl': "http://localhost:8080/?action=snapshot", + 'stream_delay_compensation': 0.05, + 'gcode_verbose': False, + 'parkhead': False, + 'parkpos': "back_right", + 'park_custom_pos_x': 10.0, + 'park_custom_pos_y': 10.0, + 'park_custom_pos_dz': 0.0, + 'park_travel_speed': 400, + 'park_retract_speed': 40, + 'park_extrude_speed': 40, + 'park_retract_distance': 0.5, + 'park_extrude_distance': 0.5, + 'park_time': 0.1, + 'fw_retract': False, + 'hyperlapse_cycle': 30, + 'autorender': True, + 'constant_rate_factor': 23, + 'output_framerate': 30, + 'pixelformat': "yuv420p", + 'time_format_code': "%d-%m-%Y_%Hh%M", + 'extraoutputparams': "", + 'variable_fps': False, + 'targetlength': 10, + 'variable_fps_min': 5, + 'variable_fps_max': 60, + 'rotation': 0, + 'flip_x': False, + 'flip_y': False, + 'duplicatelastframe': 5, + 'previewimage': True, + 'saveframes': False + } + + # Get Config from Database and overwrite defaults + dbconfig: Dict[str, Any] = self.database.get_item("timelapse", + "config", + self.config) + if isinstance(dbconfig, asyncio.Future): + self.config.update(dbconfig.result()) + else: + self.config.update(dbconfig) + + # Overwrite Config with fixed config made in moonraker.conf + # this is a fallback to older setups and when the Frontend doesn't + # support the settings endpoint + self.overwriteDbconfigWithConfighelper() + + # check if ffmpeg is installed + self.ffmpeg_installed = os.path.isfile(self.ffmpeg_binary_path) + if not self.ffmpeg_installed: + self.config['autorender'] = False + logging.info(f"timelapse: {self.ffmpeg_binary_path} \ + not found please install to use render functionality") + + # setup directories + # remove trailing "/" + out_dir_cfg = os.path.join(out_dir_cfg, '') + temp_dir_cfg = os.path.join(temp_dir_cfg, '') + # evaluate and expand "~" + self.out_dir = os.path.expanduser(out_dir_cfg) + self.temp_dir = os.path.expanduser(temp_dir_cfg) + # create directories if they doesn't exist + os.makedirs(self.temp_dir, exist_ok=True) + os.makedirs(self.out_dir, exist_ok=True) + + # setup eventhandlers and endpoints + file_manager = self.server.lookup_component("file_manager") + file_manager.register_directory("timelapse", + self.out_dir, + full_access=True + ) + file_manager.register_directory("timelapse_frames", self.temp_dir) + self.server.register_notification("timelapse:timelapse_event") + self.server.register_event_handler( + "server:gcode_response", self.handle_gcode_response) + self.server.register_event_handler( + "server:status_update", self.handle_status_update) + self.server.register_event_handler( + "server:klippy_ready", self.handle_klippy_ready) + self.server.register_remote_method( + "timelapse_newframe", self.call_newframe) + self.server.register_remote_method( + "timelapse_saveFrames", self.call_saveFramesZip) + self.server.register_remote_method( + "timelapse_render", self.call_render) + self.server.register_endpoint( + "/machine/timelapse/render", ['POST'], self.render) + self.server.register_endpoint( + "/machine/timelapse/saveframes", ['POST'], self.saveFramesZip) + self.server.register_endpoint( + "/machine/timelapse/settings", ['GET', 'POST'], + self.webrequest_settings) + self.server.register_endpoint( + "/machine/timelapse/lastframeinfo", ['GET'], + self.webrequest_lastframeinfo) + + async def component_init(self) -> None: + await self.getWebcamConfig() + + def overwriteDbconfigWithConfighelper(self) -> None: + blockedsettings = [] + + for config in self.confighelper.get_options(): + if config in self.config: + configtype = type(self.config[config]) + if configtype == str: + self.config[config] = self.confighelper.get(config) + elif configtype == bool: + self.config[config] = self.confighelper.getboolean(config) + elif configtype == int: + self.config[config] = self.confighelper.getint(config) + elif configtype == float: + self.config[config] = self.confighelper.getfloat(config) + + # add the config to list of blockedsettings + blockedsettings.append(config) + + # append the list of blockedsettings to the config dict + self.config.update({'blockedsettings': blockedsettings}) + logging.debug(f"blockedsettings {self.config['blockedsettings']}") + + async def getWebcamConfig(self) -> None: + # Read Webcam config from Database + webcam_name = self.config['camera'] + try: + wcmgr: WebcamManager = self.server.lookup_component("webcam") + cams = wcmgr.get_webcams() + + if not cams: + logging.info("WARNING: no camera configured, " + + "using the fallback config") + fallback = {'snapshot_url': self.config['snapshoturl'], + 'rotation': self.config['rotation'], + 'flip_horizontal': self.config['flip_x'], + 'flip_vertical': self.config['flip_y'] + } + self.parseWebcamConfig(fallback) + return + + if webcam_name and webcam_name in cams: + camera = cams[webcam_name] + else: + camera = list(cams.values())[0] + + self.parseWebcamConfig(camera.as_dict()) + + except Exception as e: + logging.info(f"something went wrong getting" + f"Cam Camera:{webcam_name} from Database. " + f"Exception: {e}" + ) + + def parseWebcamConfig(self, webcamconfig) -> None: + snapshoturl = webcamconfig['snapshot_url'] + flip_x = webcamconfig['flip_horizontal'] + flip_y = webcamconfig['flip_vertical'] + rotation = webcamconfig['rotation'] + + oldWebcamConfig = {"url": self.config['snapshoturl'], + "flip_x": self.config['flip_x'], + "flip_y": self.config['flip_y'], + "rotation": self.config['rotation'] + } + + self.config['snapshoturl'] = self.confighelper.get('snapshoturl', + snapshoturl + ) + self.config['flip_x'] = self.confighelper.getboolean('flip_x', + flip_x + ) + self.config['flip_y'] = self.confighelper.getboolean('flip_y', + flip_y + ) + self.config['rotation'] = self.confighelper.getint('rotation', + rotation + ) + + if not self.config['snapshoturl'].startswith('http'): + if not self.config['snapshoturl'].startswith('/'): + self.config['snapshoturl'] = "http://localhost/" + \ + self.config['snapshoturl'] + else: + self.config['snapshoturl'] = "http://localhost" + \ + self.config['snapshoturl'] + + # check if settings have changed and if so creat log entry + newWebcamConfig = {"url": self.config['snapshoturl'], + "flip_x": self.config['flip_x'], + "flip_y": self.config['flip_y'], + "rotation": self.config['rotation'] + } + + if not oldWebcamConfig == newWebcamConfig: + logging.info("snapshoturl: " + f"{self.config['snapshoturl']}, " + f"Flip V/H: {self.config['flip_y']}/" + f"{self.config['flip_y']}, " + f"rotation: {self.config['rotation']}" + ) + + async def webrequest_lastframeinfo(self, + webrequest: WebRequest + ) -> Dict[str, Any]: + return { + 'framecount': self.framecount, + 'lastframefile': self.lastframefile + } + + async def webrequest_settings(self, + webrequest: WebRequest + ) -> Dict[str, Any]: + action = webrequest.get_action() + if action == 'POST': + + args = webrequest.get_args() + logging.debug("webreq_args: " + str(args)) + + gcodechange = False + settingsWithGcodechange = [ + 'enabled', 'parkhead', + 'parkpos', 'park_custom_pos_x', + 'park_custom_pos_y', 'park_custom_pos_dz', + 'park_travel_speed', 'park_retract_speed', + 'park_extrude_speed', 'park_retract_distance', + 'park_extrude_distance', 'park_time', 'fw_retract' + ] + modechanged = False + + for setting in args: + if setting in self.config: + settingtype = type(self.config[setting]) + if setting == "snapshoturl": + logging.debug( + "snapshoturl cannot be changed via webrequest") + elif settingtype == str: + settingvalue = webrequest.get(setting) + elif settingtype == bool: + settingvalue = webrequest.get_boolean(setting) + elif settingtype == int: + settingvalue = webrequest.get_int(setting) + elif settingtype == float: + settingvalue = webrequest.get_float(setting) + + self.config[setting] = settingvalue + + self.database.insert_item( + "timelapse", + f"config.{setting}", + settingvalue + ) + + if setting == "camera": + if not self.noWebcamDb: + await self.getWebcamConfig() + else: + logging.info("Webcam Namespace not intialized, " + "please restart moonraker service!") + + if setting in settingsWithGcodechange: + gcodechange = True + + if setting == "mode": + modechanged = True + + logging.debug(f"changed setting: {setting} " + f"value: {settingvalue} " + f"type: {settingtype}" + ) + + if modechanged: + if self.config['mode'] == "hyperlapse": + if not self.hyperlapserunning: + if self.printing: + ioloop = IOLoop.current() + ioloop.spawn_callback(self.start_hyperlapse) + else: + if self.hyperlapserunning: + ioloop = IOLoop.current() + ioloop.spawn_callback(self.stop_hyperlapse) + if gcodechange: + ioloop = IOLoop.current() + ioloop.spawn_callback(self.setgcodevariables) + + return self.config + + async def handle_klippy_ready(self) -> None: + ioloop = IOLoop.current() + ioloop.spawn_callback(self.setgcodevariables) + + ioloop = IOLoop.current() + ioloop.spawn_callback(self.stop_hyperlapse) + + async def setgcodevariables(self) -> None: + gcommand = "_SET_TIMELAPSE_SETUP " \ + + f" ENABLE={self.config['enabled']}" \ + + f" VERBOSE={self.config['gcode_verbose']}" \ + + f" PARK_ENABLE={self.config['parkhead']}" \ + + f" PARK_POS={self.config['parkpos']}" \ + + f" CUSTOM_POS_X={self.config['park_custom_pos_x']}" \ + + f" CUSTOM_POS_Y={self.config['park_custom_pos_y']}" \ + + f" CUSTOM_POS_DZ={self.config['park_custom_pos_dz']}" \ + + f" TRAVEL_SPEED={self.config['park_travel_speed']}" \ + + f" RETRACT_SPEED={self.config['park_retract_speed']}" \ + + f" EXTRUDE_SPEED={self.config['park_extrude_speed']}" \ + + f" RETRACT_DISTANCE={self.config['park_retract_distance']}" \ + + f" EXTRUDE_DISTANCE={self.config['park_extrude_distance']}" \ + + f" PARK_TIME={self.config['park_time']}" \ + + f" FW_RETRACT={self.config['fw_retract']}" \ + + logging.debug(f"run gcommand: {gcommand}") + try: + await self.klippy_apis.run_gcode(gcommand) + except self.server.error: + msg = f"Error executing GCode {gcommand}" + logging.exception(msg) + + def call_newframe(self, macropark=False, hyperlapse=False) -> None: + if self.config['enabled']: + if self.config['mode'] == "hyperlapse": + if hyperlapse: + if not self.takingframe: + self.takingframe = True + self.spawn_newframe_callbacks() + else: + logging.info("last take frame hasn't completed" + + " ignoring take frame command" + ) + else: + logging.info("ignoring non hyperlapse triggered macros" + + "in hyperlapse mode" + ) + else: + self.spawn_newframe_callbacks() + else: + logging.info("NEW_FRAME macro ignored timelapse is disabled") + + def spawn_newframe_callbacks(self) -> None: + ioloop = IOLoop.current() + # release parked head after park time is passed + park_time = self.config['park_time'] + ioloop.call_later(delay=park_time, callback=self.release_parkedhead) + # capture the frame after stream delay is passed + stream_delay = self.config['stream_delay_compensation'] + ioloop.call_later(delay=stream_delay, callback=self.newframe) + + async def release_parkedhead(self) -> None: + gcommand = "SET_GCODE_VARIABLE " \ + + "MACRO=TIMELAPSE_TAKE_FRAME " \ + + "VARIABLE=takingframe VALUE=False" + + logging.debug(f"run gcommand: {gcommand}") + try: + await self.klippy_apis.run_gcode(gcommand) + except self.server.error: + msg = f"Error executing GCode {gcommand}" + logging.exception(msg) + + async def start_hyperlapse(self) -> None: + hyperlapse_cycle = self.config['hyperlapse_cycle'] + park_time = self.config['park_time'] + timediff = hyperlapse_cycle - park_time + if timediff >= 1: + gcommand = "HYPERLAPSE ACTION=START" \ + + f" CYCLE={hyperlapse_cycle}" + + logging.debug(f"run gcommand: {gcommand}") + try: + await self.klippy_apis.run_gcode(gcommand) + except self.server.error: + msg = f"Error executing GCode {gcommand}" + logging.exception(msg) + self.hyperlapserunning = True + else: + logging.info("WARNING: Blocked start of Hyperlapse, because " + f"hyperlapse_cycle ({hyperlapse_cycle}s) is smaller " + f"then or to close to park_time ({park_time}s)" + ) + + async def stop_hyperlapse(self) -> None: + gcommand = "HYPERLAPSE ACTION=STOP" + + logging.debug(f"run gcommand: {gcommand}") + try: + await self.klippy_apis.run_gcode(gcommand) + except self.server.error: + msg = f"Error executing GCode {gcommand}" + logging.exception(msg) + self.hyperlapserunning = False + + async def newframe(self) -> None: + # make sure webcamconfig is uptodate before grabbing a new frame + await self.getWebcamConfig() + + options = "" + if self.wget_skip_cert: + options += "--no-check-certificate " + + self.framecount += 1 + framefile = "frame" + str(self.framecount).zfill(6) + ".jpg" + cmd = "wget " + options + self.config['snapshoturl'] \ + + " -O " + self.temp_dir + framefile + self.lastframefile = framefile + logging.debug(f"cmd: {cmd}") + + shell_cmd: SCMDComp = self.server.lookup_component('shell_command') + scmd = shell_cmd.build_shell_command(cmd, None) + try: + cmdstatus = await scmd.run(timeout=2., verbose=False) + except Exception: + logging.exception(f"Error running cmd '{cmd}'") + + result = {'action': 'newframe'} + if cmdstatus: + result.update({ + 'frame': str(self.framecount), + 'framefile': framefile, + 'status': 'success' + }) + else: + logging.info(f"getting newframe failed: {cmd}") + self.framecount -= 1 + result.update({'status': 'error'}) + + self.notify_event(result) + self.takingframe = False + + async def handle_status_update(self, status: Dict[str, Any]) -> None: + if 'print_stats' in status: + printstats = status['print_stats'] + if 'state' in printstats: + state = printstats['state'] + if state == 'cancelled': + self.printing = False + ioloop = IOLoop.current() + ioloop.spawn_callback(self.stop_hyperlapse) + + async def handle_gcode_response(self, gresponse: str) -> None: + if gresponse == "File selected": + # print_started + self.cleanup() + self.printing = True + + # start hyperlapse if mode is set + if self.config['mode'] == "hyperlapse": + ioloop = IOLoop.current() + ioloop.spawn_callback(self.start_hyperlapse) + + elif gresponse == "Done printing file": + # print_done + self.printing = False + + # stop hyperlapse if mode is set + if self.config['mode'] == "hyperlapse": + ioloop = IOLoop.current() + ioloop.spawn_callback(self.stop_hyperlapse) + + if self.config['enabled']: + if self.config['saveframes']: + ioloop = IOLoop.current() + ioloop.spawn_callback(self.saveFramesZip) + if self.config['autorender']: + ioloop = IOLoop.current() + ioloop.spawn_callback(self.render) + + def cleanup(self) -> None: + logging.debug("cleanup frame directory") + filelist = glob.glob(self.temp_dir + "frame*.jpg") + if filelist: + for filepath in filelist: + os.remove(filepath) + self.framecount = 0 + self.lastframefile = "" + + def call_saveFramesZip(self) -> None: + ioloop = IOLoop.current() + ioloop.spawn_callback(self.saveFramesZip) + + async def saveFramesZip(self, webrequest=None): + filelist = sorted(glob.glob(self.temp_dir + "frame*.jpg")) + self.framecount = len(filelist) + result = {'action': 'saveframes'} + + if not filelist: + msg = "no frames to save, skip" + status = "skipped" + elif self.saveisrunning: + msg = "saving frames already" + status = "running" + else: + self.saveisrunning = True + + # get printed filename + kresult = await self.klippy_apis.query_objects( + {'print_stats': None}) + pstats = kresult.get("print_stats", {}) + gcodefilename = pstats.get("filename", "").split("/")[-1] + + # prepare output filename + now = datetime.now() + date_time = now.strftime(self.config['time_format_code']) + outfile = f"k1_{gcodefilename}_{date_time}" + outfileFull = outfile + "_frames.zip" + + zipObj = ZipFile(self.out_dir + outfileFull, "w") + + for frame in filelist: + zipObj.write(frame, frame.split("/")[-1]) + + logging.info(f"saved frames: {outfile}_frames.zip") + + result.update({ + 'status': 'finished', + 'zipfile': outfileFull + }) + + self.saveisrunning = False + + return result + + def call_render(self, byrendermacro=False) -> None: + self.byrendermacro = byrendermacro + ioloop = IOLoop.current() + ioloop.spawn_callback(self.render) + + async def render(self, webrequest=None): + filelist = sorted(glob.glob(self.temp_dir + "frame*.jpg")) + self.framecount = len(filelist) + result = {'action': 'render'} + + # make sure webcamconfig is uptodate for the rotation/flip feature + await self.getWebcamConfig() + + if not filelist: + msg = "no frames to render, skip" + status = "skipped" + elif self.renderisrunning: + msg = "render is already running" + status = "running" + elif not self.ffmpeg_installed: + msg = f"{self.ffmpeg_binary_path} not found, please install ffmpeg" + status = "error" + # cmd = outfile = None + logging.info(f"timelapse: {msg}") + else: + self.renderisrunning = True + + # get printed filename + kresult = await self.klippy_apis.query_objects( + {'print_stats': None}) + pstats = kresult.get("print_stats", {}) + gcodefilename = pstats.get("filename", "").split("/")[-1] + + # prepare output filename + now = datetime.now() + date_time = now.strftime(self.config['time_format_code']) + inputfiles = self.temp_dir + "frame%6d.jpg" + outfile = f"k1_{gcodefilename}_{date_time}" + + # dublicate last frame + duplicates = [] + if self.config['duplicatelastframe'] > 0: + lastframe = filelist[-1:][0] + + for i in range(self.config['duplicatelastframe']): + nextframe = str(self.framecount + i + 1).zfill(6) + duplicate = "frame" + nextframe + ".jpg" + duplicatePath = self.temp_dir + duplicate + duplicates.append(duplicatePath) + try: + shutil.copy(lastframe, duplicatePath) + except OSError as err: + logging.info(f"duplicating last frame failed: {err}") + + # update Filelist + filelist = sorted(glob.glob(self.temp_dir + "frame*.jpg")) + self.framecount = len(filelist) + + # variable framerate + if self.config['variable_fps']: + fps = int(self.framecount / self.config['targetlength']) + fps = max(min(fps, + self.config['variable_fps_max']), + self.config['variable_fps_min']) + else: + fps = self.config['output_framerate'] + + # apply rotation + filterParam = "" + if self.config['rotation'] == 90 and self.config['flip_y']: + filterParam = " -vf 'transpose=3'" + elif self.config['rotation'] == 90: + filterParam = " -vf 'transpose=1'" + elif self.config['rotation'] == 180: + filterParam = " -vf 'hflip,vflip'" + elif self.config['rotation'] == 270: + filterParam = " -vf 'transpose=2'" + elif self.config['rotation'] == 270 and self.config['flip_y']: + filterParam = " -vf 'transpose=0'" + elif self.config['rotation'] > 0: + pi = 3.141592653589793 + rot = str(self.config['rotation']*(pi/180)) + filterParam = " -vf 'rotate=" + rot + "'" + elif self.config['flip_x'] and self.config['flip_y']: + filterParam = " -vf 'hflip,vflip'" + elif self.config['flip_x']: + filterParam = " -vf 'hflip'" + elif self.config['flip_y']: + filterParam = " -vf 'vflip'" + + # build shell command + cmd = self.ffmpeg_binary_path \ + + " -r " + str(fps) \ + + " -i '" + inputfiles + "'" \ + + filterParam \ + + " -threads 2 -g 5" \ + + " -crf " + str(self.config['constant_rate_factor']) \ + + " -vcodec libx264" \ + + " -pix_fmt " + self.config['pixelformat'] \ + + " -preset superfast" \ + + " -an" \ + + " " + self.config['extraoutputparams'] \ + + " '" + self.temp_dir + outfile + ".mp4' -y" + + # log and notify ws + logging.info(f"start FFMPEG: {cmd}") + result.update({ + 'status': 'started', + 'framecount': str(self.framecount), + 'settings': { + 'framerate': fps, + 'crf': self.config['constant_rate_factor'], + 'pixelformat': self.config['pixelformat'] + } + }) + + # run the command + shell_cmd: SCMDComp = self.server.lookup_component('shell_command') + self.notify_event(result) + scmd = shell_cmd.build_shell_command(cmd, self.ffmpeg_cb) + try: + cmdstatus = await scmd.run(verbose=True, + log_complete=False, + timeout=9999999999, + ) + except Exception: + logging.exception(f"Error running cmd '{cmd}'") + + # check success + if cmdstatus: + status = "success" + msg = f"Rendering Video successful: {outfile}.mp4" + result.update({ + 'filename': f"{outfile}.mp4", + 'printfile': gcodefilename + }) + # result.pop("framecount") + result.pop("settings") + + # move finished output file to output directory + try: + shutil.move(self.temp_dir + outfile + ".mp4", + self.out_dir + outfile + ".mp4") + except OSError as err: + logging.info(f"moving output file failed: {err}") + + # copy image preview + if self.config['previewimage']: + previewFile = f"{outfile}.jpg" + previewFilePath = self.out_dir + previewFile + previewSrc = filelist[-1:][0] + try: + shutil.copy(previewSrc, previewFilePath) + except OSError as err: + logging.info(f"copying preview image failed: {err}") + else: + result.update({ + 'previewimage': previewFile + }) + + # apply rotation previewimage if needed + if filterParam or self.config['extraoutputparams']: + cmd = self.ffmpeg_binary_path \ + + " -i '" + previewFilePath + "'" \ + + filterParam \ + + " -an" \ + + " " + self.config['extraoutputparams'] \ + + " '" + previewFilePath + "' -y" + + logging.info(f"Rotate preview image cmd: {cmd}") + + scmd = shell_cmd.build_shell_command(cmd) + try: + cmdstatus = await scmd.run(verbose=True, + log_complete=False, + timeout=9999999999, + ) + except Exception: + logging.exception(f"Error running cmd '{cmd}'") + + else: + status = "error" + msg = f"Rendering Video failed: {cmd} : {self.lastcmdreponse}" + result.update({ + 'cmd': cmd, + 'cmdresponse': self.lastcmdreponse + }) + + self.renderisrunning = False + + # cleanup duplicates + if duplicates: + for dupe in duplicates: + try: + os.remove(dupe) + except OSError as err: + logging.info(f"remove duplicate failed: {err}") + + # log and notify ws + logging.info(msg) + result.update({ + 'status': status, + 'msg': msg + }) + self.notify_event(result) + + # confirm render finish to stop the render macro loop + if self.byrendermacro: + gcommand = "SET_GCODE_VARIABLE " \ + + "MACRO=TIMELAPSE_RENDER VARIABLE=render VALUE=False" + logging.debug(f"run gcommand: {gcommand}") + try: + await self.klippy_apis.run_gcode(gcommand) + except self.server.error: + msg = f"Error executing GCode {gcommand}" + logging.exception(msg) + self.byrendermacro = False + + return result + + def ffmpeg_cb(self, response): + # logging.debug(f"ffmpeg_cb: {response}") + self.lastcmdreponse = response.decode("utf-8") + try: + frame = re.search( + r'(?<=frame=)*(\d+)(?=.+fps)', self.lastcmdreponse + ).group() + except AttributeError: + return + percent = int(frame) / self.framecount * 100 + if percent > 100: + percent = 100 + + if self.lastrenderprogress != int(percent): + self.lastrenderprogress = int(percent) + # logging.debug(f"ffmpeg Progress: {self.lastrenderprogress}% ") + result = { + 'action': 'render', + 'status': 'running', + 'progress': self.lastrenderprogress + } + self.notify_event(result) + + def notify_event(self, result: Dict[str, Any]) -> None: + logging.debug(f"notify_event: {result}") + self.server.send_event("timelapse:timelapse_event", result) + + +def load_component(config: ConfigHelper) -> Timelapse: + return Timelapse(config) diff --git a/files/moonraker/moonraker.asvc b/files/moonraker/moonraker.asvc new file mode 100644 index 0000000..ece878f --- /dev/null +++ b/files/moonraker/moonraker.asvc @@ -0,0 +1,13 @@ +klipper_mcu +webcamd +MoonCord +KlipperScreen +moonraker-telegram-bot +moonraker-obico +sonar +crowsnest +octoeverywhere +ratos-configurator +mobileraker +guppyscreen +Git-Backup \ No newline at end of file diff --git a/files/moonraker/moonraker.conf b/files/moonraker/moonraker.conf new file mode 100644 index 0000000..9a68a12 --- /dev/null +++ b/files/moonraker/moonraker.conf @@ -0,0 +1,103 @@ +[server] +host: 0.0.0.0 +port: 7125 +klippy_uds_address: /tmp/klippy_uds +max_upload_size: 1024 + +[file_manager] +queue_gcode_uploads: False +enable_object_processing: True + +[database] + +[data_store] +temperature_store_size: 600 +gcode_store_size: 1000 + +[machine] +provider: supervisord_cli +validate_service: False +validate_config: False + +[authorization] +force_logins: False +cors_domains: + *.lan + *.local + *://localhost + *://localhost:* + *://my.mainsail.xyz + *://app.fluidd.xyz + +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 + +[octoprint_compat] + +[history] + +[update_manager] +enable_auto_refresh: True +refresh_interval: 24 +enable_system_updates: False + +# Remove '#' after this line to keep Creality Helper Script up to date +[update_manager Creality-Helper-Script] +type: git_repo +channel: dev +path: /usr/data/helper-script +origin: https://github.com/Guilouz/Creality-Helper-Script.git +primary_branch: master +managed_services: klipper + +# Remove '#' after this line to enable camera configuration with Moonraker and replace 'xxx.xxx.xxx.xxx' by your IP addresses +#[webcam Camera] +#location: printer +#enabled: True +#service: mjpegstreamer +#target_fps: 15 +#target_fps_idle: 5 +#stream_url: http://xxx.xxx.xxx.xxx:8080/?action=stream +#snapshot_url: http://xxx.xxx.xxx.xxx:8080/?action=snapshot +#flip_horizontal: False +#flip_vertical: False +#rotation: 0 +#aspect_ratio: 4:3 + +# Remove '#' after this line if you use Timelapse function and replace port '4408' by '4409' in snapshoturl if you use Mainsail +#[timelapse] +#output_path: /usr/data/printer_data/timelapse/ +#frame_path: /usr/data/printer_data/frames/ +#ffmpeg_binary_path: /opt/bin/ffmpeg +#snapshoturl: http://localhost:8080/?action=snapshot + +# Remove '#' after this line if you use Fluidd +#[update_manager fluidd] +#type: web +#channel: beta +#repo: fluidd-core/fluidd +#path: /usr/data/fluidd + +# Remove '#' after this line if you use Mainsail +#[update_manager mainsail] +#type: web +#channel: beta +#repo: mainsail-crew/mainsail +#path: /usr/data/mainsail + +# Remove '#' after this line if you use Mobileraker Companion +#[update_manager mobileraker] +#type: git_repo +#path: /usr/data/mobileraker_companion +#origin: https://github.com/Clon1998/mobileraker_companion.git +#virtualenv: /usr/data/mobileraker-env +#primary_branch:main +#requirements: scripts/mobileraker-requirements.txt +#install_script: scripts/install.sh +#managed_services: mobileraker diff --git a/files/moonraker/moonraker.tar.gz b/files/moonraker/moonraker.tar.gz new file mode 100644 index 0000000..3fbe80b Binary files /dev/null and b/files/moonraker/moonraker.tar.gz differ diff --git a/files/nozzle-cleaning-fan-control/__init__.py b/files/nozzle-cleaning-fan-control/__init__.py new file mode 100644 index 0000000..39b6264 --- /dev/null +++ b/files/nozzle-cleaning-fan-control/__init__.py @@ -0,0 +1,5 @@ + +from .prtouch_v2_fan import PRTouchFan + +def load_config(config): + return PRTouchFan(config) diff --git a/files/nozzle-cleaning-fan-control/nozzle-cleaning-fan-control.cfg b/files/nozzle-cleaning-fan-control/nozzle-cleaning-fan-control.cfg new file mode 100644 index 0000000..38d6104 --- /dev/null +++ b/files/nozzle-cleaning-fan-control/nozzle-cleaning-fan-control.cfg @@ -0,0 +1,6 @@ +######################################## +# Nozzle Cleaning Fan Control +######################################## + +[prtouch_v2_fan] +max_speed: 0.5 diff --git a/files/nozzle-cleaning-fan-control/prtouch_v2_fan.pyc b/files/nozzle-cleaning-fan-control/prtouch_v2_fan.pyc new file mode 100644 index 0000000..c48bb93 Binary files /dev/null and b/files/nozzle-cleaning-fan-control/prtouch_v2_fan.pyc differ diff --git a/files/screws-tilt-adjust/screws-tilt-adjust-k1.cfg b/files/screws-tilt-adjust/screws-tilt-adjust-k1.cfg new file mode 100644 index 0000000..bf06e99 --- /dev/null +++ b/files/screws-tilt-adjust/screws-tilt-adjust-k1.cfg @@ -0,0 +1,25 @@ +######################################## +# Screws Tilt Adjust for K1 +######################################## + +[screws_tilt_adjust] +screw1: 25,20 +screw1_name: front left screw +screw2: 195,20 +screw2_name: front right screw +screw3: 195,190 +screw3_name: rear right screw +screw4: 25,190 +screw4_name: rear left screw +speed: 100 +horizontal_move_z: 5 +screw_thread: CW-M4 + + +[gcode_macro SCREWS_CALIBRATION] +description: Start Bed Screws Calibration +gcode: + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + SCREWS_TILT_CALCULATE \ No newline at end of file diff --git a/files/screws-tilt-adjust/screws-tilt-adjust-k1max.cfg b/files/screws-tilt-adjust/screws-tilt-adjust-k1max.cfg new file mode 100644 index 0000000..5da9943 --- /dev/null +++ b/files/screws-tilt-adjust/screws-tilt-adjust-k1max.cfg @@ -0,0 +1,25 @@ +######################################## +# Screws Tilt Adjust for K1 Max +######################################## + +[screws_tilt_adjust] +screw1: 19,23 +screw1_name: front left screw +screw2: 278,23 +screw2_name: front right screw +screw3: 248,272 +screw3_name: rear right screw +screw4: 48,272 +screw4_name: rear left screw +horizontal_move_z: 5 +speed: 150 +screw_thread: CW-M4 + + +[gcode_macro SCREWS_CALIBRATION] +description: Start Bed Screws Calibration +gcode: + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + SCREWS_TILT_CALCULATE diff --git a/files/screws-tilt-adjust/screws_tilt_adjust.py b/files/screws-tilt-adjust/screws_tilt_adjust.py new file mode 100644 index 0000000..521744a --- /dev/null +++ b/files/screws-tilt-adjust/screws_tilt_adjust.py @@ -0,0 +1,131 @@ +# Helper script to adjust bed screws tilt using Z probe +# +# Copyright (C) 2019 Rui Caridade +# Copyright (C) 2021 Matthew Lloyd +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import math +from . import probe + +class ScrewsTiltAdjust: + def __init__(self, config): + self.config = config + self.printer = config.get_printer() + self.screws = [] + self.results = [] + self.max_diff = None + self.max_diff_error = False + # Read config + for i in range(99): + prefix = "screw%d" % (i + 1,) + if config.get(prefix, None) is None: + break + screw_coord = config.getfloatlist(prefix, count=2) + screw_name = "screw at %.3f,%.3f" % screw_coord + screw_name = config.get(prefix + "_name", screw_name) + self.screws.append((screw_coord, screw_name)) + if len(self.screws) < 3: + raise config.error("screws_tilt_adjust: Must have " + "at least three screws") + self.threads = {'CW-M3': 0, 'CCW-M3': 1, 'CW-M4': 2, 'CCW-M4': 3, + 'CW-M5': 4, 'CCW-M5': 5, 'CW-M6': 6, 'CCW-M6': 7} + self.thread = config.getchoice('screw_thread', self.threads, + default='CW-M3') + # Initialize ProbePointsHelper + points = [coord for coord, name in self.screws] + self.probe_helper = probe.ProbePointsHelper(self.config, + self.probe_finalize, + default_points=points) + self.probe_helper.minimum_points(3) + # Register command + self.gcode = self.printer.lookup_object('gcode') + self.gcode.register_command("SCREWS_TILT_CALCULATE", + self.cmd_SCREWS_TILT_CALCULATE, + desc=self.cmd_SCREWS_TILT_CALCULATE_help) + cmd_SCREWS_TILT_CALCULATE_help = "Tool to help adjust bed leveling " \ + "screws by calculating the number " \ + "of turns to level it." + + def cmd_SCREWS_TILT_CALCULATE(self, gcmd): + self.max_diff = gcmd.get_float("MAX_DEVIATION", None) + # Option to force all turns to be in the given direction (CW or CCW) + direction = gcmd.get("DIRECTION", default=None) + if direction is not None: + direction = direction.upper() + if direction not in ('CW', 'CCW'): + raise gcmd.error( + "Error on '%s': DIRECTION must be either CW or CCW" % ( + gcmd.get_commandline(),)) + self.direction = direction + self.probe_helper.start_probe(gcmd) + + def get_status(self, eventtime): + return {'error': self.max_diff_error, + 'max_deviation': self.max_diff, + 'results': self.results} + + def probe_finalize(self, offsets, positions): + self.results = {} + self.max_diff_error = False + # Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M6 + #and CCW-M6 + threads_factor = {0: 0.5, 1: 0.5, 2: 0.7, 3: 0.7, 4: 0.8, 5: 0.8, + 6: 1.0, 7: 1.0} + is_clockwise_thread = (self.thread & 1) == 0 + screw_diff = [] + # Process the read Z values + if self.direction is not None: + # Lowest or highest screw is the base position used for comparison + use_max = ((is_clockwise_thread and self.direction == 'CW') + or (not is_clockwise_thread and self.direction == 'CCW')) + min_or_max = max if use_max else min + i_base, z_base = min_or_max( + enumerate([pos[2] for pos in positions]), key=lambda v: v[1]) + else: + # First screw is the base position used for comparison + i_base, z_base = 0, positions[0][2] + # Provide the user some information on how to read the results + self.gcode.respond_info("01:20 means 1 full turn and 20 minutes, " + "CW=clockwise, CCW=counter-clockwise") + for i, screw in enumerate(self.screws): + z = positions[i][2] + coord, name = screw + if i == i_base: + # Show the results + self.gcode.respond_info( + "%s : x=%.1f, y=%.1f, z=%.5f" % + (name + ' (base)', coord[0], coord[1], z)) + sign = "CW" if is_clockwise_thread else "CCW" + self.results["screw%d" % (i + 1,)] = {'z': z, 'sign': sign, + 'adjust': '00:00', 'is_base': True} + else: + # Calculate how knob must be adjusted for other positions + diff = z_base - z + screw_diff.append(abs(diff)) + if abs(diff) < 0.001: + adjust = 0 + else: + adjust = diff / threads_factor.get(self.thread, 0.5) + if is_clockwise_thread: + sign = "CW" if adjust >= 0 else "CCW" + else: + sign = "CCW" if adjust >= 0 else "CW" + adjust = abs(adjust) + full_turns = math.trunc(adjust) + decimal_part = adjust - full_turns + minutes = round(decimal_part * 60, 0) + # Show the results + self.gcode.respond_info( + "%s : x=%.1f, y=%.1f, z=%.5f : adjust %s %02d:%02d" % + (name, coord[0], coord[1], z, sign, full_turns, minutes)) + self.results["screw%d" % (i + 1,)] = {'z': z, 'sign': sign, + 'adjust':"%02d:%02d" % (full_turns, minutes), + 'is_base': False} + if self.max_diff and any((d > self.max_diff) for d in screw_diff): + self.max_diff_error = True + raise self.gcode.error( + "bed level exceeds configured limits ({}mm)! " \ + "Adjust screws and restart print.".format(self.max_diff)) + +def load_config(config): + return ScrewsTiltAdjust(config) diff --git a/files/scripts/useful_macros.sh b/files/scripts/useful_macros.sh new file mode 100755 index 0000000..4479242 --- /dev/null +++ b/files/scripts/useful_macros.sh @@ -0,0 +1,72 @@ +#!/bin/sh + +set -e + +function backup_klipper(){ + if [ -f /usr/data/printer_data/config/backup_config.tar.gz ]; then + rm -f /usr/data/printer_data/config/backup_config.tar.gz + fi + cd /usr/data/printer_data + echo -e "Info: Compressing files..." + tar -czvf /usr/data/printer_data/config/backup_config.tar.gz config + echo -e "Info: Klipper configuration files have been saved successfully!" + exit 0 +} + +function restore_klipper(){ + if [ ! -f /usr/data/printer_data/config/backup_config.tar.gz ]; then + echo -e "Info: Please backup Klipper configuration files before restore!" + exit 1 + fi + cd /usr/data/printer_data + mv config/backup_config.tar.gz backup_config.tar.gz + if [ -d config ]; then + rm -rf config + fi + echo -e "Info: Restoring files..." + tar -xvf backup_config.tar.gz + mv backup_config.tar.gz config/backup_config.tar.gz + echo -e "Info: Klipper configuration files have been restored successfully!" + exit 0 +} + +function backup_moonraker(){ + if [ -f /usr/data/printer_data/config/backup_database.tar.gz ]; then + rm -f /usr/data/printer_data/config/backup_database.tar.gz + fi + cd /usr/data/printer_data + echo -e "Info: Compressing files..." + tar -czvf /usr/data/printer_data/config/backup_database.tar.gz database + echo -e "Info: Moonraker database has been saved successfully!" + exit 0 +} + +function restore_moonraker(){ + if [ ! -f /usr/data/printer_data/config/backup_database.tar.gz ]; then + echo -e "Info: Please backup Moonraker database before restore!" + exit 1 + fi + cd /usr/data/printer_data + mv config/backup_database.tar.gz backup_database.tar.gz + if [ -d database ]; then + rm -rf database + fi + echo -e "Info: Restoring files..." + tar -xvf backup_database.tar.gz + mv backup_database.tar.gz config/backup_database.tar.gz + echo -e "Info: Moonraker database has been restored successfully!" + exit 0 +} + +if [ "$1" == "-backup_klipper" ]; then + backup_klipper +elif [ "$1" == "-restore_klipper" ]; then + restore_klipper +elif [ "$1" == "-backup_moonraker" ]; then + backup_moonraker +elif [ "$1" == "-restore_moonraker" ]; then + restore_moonraker +else + echo -e "Invalid argument. Usage: $0 [-backup_klipper | -restore_klipper | -backup_moonraker | -restore_moonraker]" + exit 1 +fi \ No newline at end of file diff --git a/files/services/S50nginx b/files/services/S50nginx new file mode 100755 index 0000000..53cf166 --- /dev/null +++ b/files/services/S50nginx @@ -0,0 +1,32 @@ +#!/bin/sh +# +# Start/stop nginx +# + +NGINX=/usr/data/nginx/sbin/nginx +PIDFILE=/var/run/nginx.pid +NGINX_ARGS="-c /usr/data/nginx/nginx/nginx.conf" + +case "$1" in + start) + echo "Starting nginx..." + mkdir -p /var/log/nginx /var/tmp/nginx + start-stop-daemon -S -p "$PIDFILE" --exec "$NGINX" -- $NGINX_ARGS + ;; + stop) + echo "Stopping nginx..." + start-stop-daemon -K -x "$NGINX" -p "$PIDFILE" -o + ;; + reload|force-reload) + echo "Reloading nginx configuration..." + "$NGINX" -s reload + ;; + restart) + "$0" stop + sleep 1 # Prevent race condition: ensure nginx stops before start. + "$0" start + ;; + *) + echo "Usage: $0 {start|stop|restart|reload|force-reload}" + exit 1 +esac diff --git a/files/services/S55klipper_service b/files/services/S55klipper_service new file mode 100755 index 0000000..fce9a34 --- /dev/null +++ b/files/services/S55klipper_service @@ -0,0 +1,54 @@ +#!/bin/sh +# +# Starts klipper service. +# + +USER_DATA=/usr/data +PROG=/usr/share/klippy-env/bin/python +PY_SCRIPT=/usr/share/klipper/klippy/klippy.py +PRINTER_DATA_DIR=$USER_DATA/printer_data +PRINTER_CONFIG_DIR=$PRINTER_DATA_DIR/config +PRINTER_LOGS_DIR=$PRINTER_DATA_DIR/logs +PID_FILE=/var/run/klippy.pid + +mcu_reset() +{ + [ -z $(pidof klipper_mcu) ] || /etc/init.d/S57klipper_mcu restart +} + +start() { + + mcu_reset + + HOME=/root start-stop-daemon -S -q -b -m -p $PID_FILE \ + --exec $PROG -- $PY_SCRIPT \ + $PRINTER_CONFIG_DIR/printer.cfg \ + -l $PRINTER_LOGS_DIR/klippy.log \ + -a /tmp/klippy_uds +} + +stop() { + start-stop-daemon -K -q -p $PID_FILE +} + +restart() { + stop + start +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart|reload) + restart + ;; + *) + echo "Usage: $0 {start|stop|restart}" + exit 1 +esac + +exit $? diff --git a/files/services/S56moonraker_service b/files/services/S56moonraker_service new file mode 100755 index 0000000..e0da407 --- /dev/null +++ b/files/services/S56moonraker_service @@ -0,0 +1,51 @@ +#!/bin/sh +# +# Starts moonraker service. +# + +USER_DATA=/usr/data +PROG=/usr/data/moonraker/moonraker-env/bin/python +PY_SCRIPT=/usr/data/moonraker/moonraker/moonraker/moonraker.py +DEFAULT_CFG=/usr/data/moonraker//moonraker/moonraker.conf +PRINTER_DATA_DIR=$USER_DATA/printer_data +PRINTER_CONFIG_DIR=$PRINTER_DATA_DIR/config +PRINTER_LOGS_DIR=$PRINTER_DATA_DIR/logs +PID_FILE=/var/run/moonraker.pid + + +start() { + + [ -d $PRINTER_DATA_DIR ] || mkdir -p $PRINTER_DATA_DIR + [ -d $PRINTER_CONFIG_DIR ] || mkdir -p $PRINTER_CONFIG_DIR + [ -d $PRINTER_LOGS_DIR ] || mkdir -p $PRINTER_LOGS_DIR + [ -s $PRINTER_CONFIG_DIR/moonraker.conf ] || cp $DEFAULT_CFG $PRINTER_CONFIG_DIR/moonraker.conf + + rm -rf /usr/data/moonraker/tmp; mkdir -p /usr/data/moonraker/tmp + TMPDIR=/usr/data/moonraker/tmp HOME=/root start-stop-daemon -S -q -b -m -p $PID_FILE \ + --exec $PROG -- $PY_SCRIPT -d $PRINTER_DATA_DIR +} +stop() { + start-stop-daemon -K -q -p $PID_FILE +} +restart() { + stop + sleep 1 + start +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart|reload) + restart + ;; + *) + echo "Usage: $0 {start|stop|restart}" + exit 1 +esac + +exit $? diff --git a/helper.sh b/helper.sh new file mode 100755 index 0000000..7e06157 --- /dev/null +++ b/helper.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +set -e +clear + +HELPER_SCRIPT_FOLDER="$( cd "$( dirname "${0}" )" && pwd )" +for script in "${HELPER_SCRIPT_FOLDER}/scripts/"*.sh; do . "${script}"; done +for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/"*.sh; do . "${script}"; done +for script in "${HELPER_SCRIPT_FOLDER}/scripts/menu/KE/"*.sh; do . "${script}"; done + +function update_helper_script() { + echo -e "${white}" + echo -e "Info: Updating Creality Helper Script..." + cd "${HELPER_SCRIPT_FOLDER}" + git reset --hard && git pull + ok_msg "Creality Helper Script has been updated!" + echo -e " ${green}Please restart script to load the new version.${white}" + echo + exit 0 +} + +function update_available() { + [[ ! -d "${HELPER_SCRIPT_FOLDER}/.git" ]] && return + local remote current + cd "${HELPER_SCRIPT_FOLDER}" + ! git branch -a | grep -q "\* main" && return + git fetch -q > /dev/null 2>&1 + remote=$(git rev-parse --short=8 FETCH_HEAD) + current=$(git rev-parse --short=8 HEAD) + if [[ ${remote} != "${current}" ]]; then + echo "true" + fi +} + +function update_menu() { + local update_available=$(update_available) + if [[ "$update_available" == "true" ]]; then + top_line + title "A new script version is available!" "${green}" + inner_line + hr + echo -e " │ ${cyan}It's recommended to keep script up to date. Updates usually ${white}│" + echo -e " │ ${cyan}contain bug fixes, important changes or new features. ${white}│" + echo -e " │ ${cyan}Please consider updating! ${white}│" + hr + echo -e " │ See changelog here: ${yellow}https://tinyurl.com/223jc4zr ${white}│" + hr + bottom_line + local yn + while true; do + read -p " Do you want to update now? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + run "update_helper_script" + break;; + N|n) + break;; + *) + error_msg "Please select a correct choice!";; + esac + done + fi +} + +rm -rf /root/.cache +set_paths +set_permissions +update_menu +main_menu diff --git a/scripts/backup_klipper_config.sh b/scripts/backup_klipper_config.sh new file mode 100755 index 0000000..24d99e3 --- /dev/null +++ b/scripts/backup_klipper_config.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +set -e + +function backup_klipper_config_files_message(){ + top_line + title 'Backup Klipper configuration files' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to backup Klipper configuration files in a ${white}│" + echo -e " │ ${cyan}backup_config.tar.gz compressed file. ${white}│" + hr + bottom_line +} + +function restore_klipper_config_files_message(){ + top_line + title 'Restore Klipper configuration files' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to restore Klipper configuration files from a ${white}│" + echo -e " │ ${cyan}backup_config.tar.gz compressed file. ${white}│" + hr + bottom_line +} + +function backup_klipper_config_files(){ + backup_klipper_config_files_message + local yn + while true; do + backup_msg "Klipper configuration files" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$KLIPPER_CONFIG_FOLDER"/backup_config.tar.gz ]; then + rm -f "$KLIPPER_CONFIG_FOLDER"/backup_config.tar.gz + fi + cd "$PRINTER_DATA_FOLDER" + echo -e "Info: Compressing files..." + tar -czvf "$KLIPPER_CONFIG_FOLDER"/backup_config.tar.gz config + ok_msg "Klipper configuration files have been saved successfully in ${yellow}/usr/data/printer_data/config ${green}folder!" + return;; + N|n) + error_msg "Backup canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function restore_klipper_config_files(){ + restore_klipper_config_files_message + local yn + while true; do + restore_msg "Klipper configuration files" yn + case "${yn}" in + Y|y) + echo -e "${white}" + cd "$PRINTER_DATA_FOLDER" + mv config/backup_config.tar.gz backup_config.tar.gz + if [ -d config ]; then + rm -rf config + fi + echo -e "Info: Restoring files..." + tar -xvf backup_config.tar.gz + mv backup_config.tar.gz config/backup_config.tar.gz + ok_msg "Klipper configuration files have been restored successfully!" + return;; + N|n) + error_msg "Restoration canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/backup_moonraker_database.sh b/scripts/backup_moonraker_database.sh new file mode 100755 index 0000000..3f937c6 --- /dev/null +++ b/scripts/backup_moonraker_database.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +set -e + +function backup_moonraker_database_message(){ + top_line + title 'Backup Moonraker database' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to backup Moonraker database in a ${white}│" + echo -e " │ ${cyan}backup_database.tar.gz compressed file. ${white}│" + hr + bottom_line +} + +function restore_moonraker_database_message(){ + top_line + title 'Restore Moonraker database' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to restore Moonraker database from a ${white}│" + echo -e " │ ${cyan}backup_database.tar.gz compressed file. ${white}│" + hr + bottom_line +} + +function backup_moonraker_database(){ + backup_moonraker_database_message + local yn + while true; do + backup_msg "Moonraker database" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$KLIPPER_CONFIG_FOLDER"/backup_database.tar.gz ]; then + rm -f "$KLIPPER_CONFIG_FOLDER"/backup_database.tar.gz + fi + cd "$PRINTER_DATA_FOLDER" + echo -e "Info: Compressing files..." + tar -czvf "$KLIPPER_CONFIG_FOLDER"/backup_database.tar.gz database + ok_msg "Moonraker database has been saved successfully in ${yellow}/usr/data/printer_data/config ${green}folder!" + return;; + N|n) + error_msg "Backup canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function restore_moonraker_database(){ + restore_moonraker_database_message + local yn + while true; do + restore_msg "Moonraker database" yn + case "${yn}" in + Y|y) + echo -e "${white}" + cd "$PRINTER_DATA_FOLDER" + mv config/backup_database.tar.gz backup_database.tar.gz + if [ -d database ]; then + rm -rf database + fi + echo -e "Info: Restoring files..." + tar -xvf backup_database.tar.gz + mv backup_database.tar.gz config/backup_database.tar.gz + ok_msg "Moonraker database has been restored successfully!" + return;; + N|n) + error_msg "Restoration canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/buzzer_support.sh b/scripts/buzzer_support.sh new file mode 100755 index 0000000..ecd0064 --- /dev/null +++ b/scripts/buzzer_support.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +set -e + +function buzzer_support_message(){ + top_line + title 'Buzzer Support' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to play sounds using the motherboard buzzer. ${white}│" + hr + bottom_line +} + +function install_buzzer_support(){ + buzzer_support_message + local yn + while true; do + install_msg "Buzzer Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/buzzer-support.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/buzzer-support.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Linking file..." + ln -sf "$BUZZER_URL" "$HS_CONFIG_FOLDER"/buzzer-support.cfg + if grep -q "include Helper-Script/buzzer-support" "$PRINTER_CFG" ; then + echo -e "Info: Buzzer Support configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Buzzer Support configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/buzzer-support\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Buzzer Support has been installed successfully!" + echo -e " You can now use ${yellow}BEEP ${white}command in your macros to play sound." + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_buzzer_support(){ + buzzer_support_message + local yn + while true; do + remove_msg "Buzzer Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing file..." + rm -f "$HS_CONFIG_FOLDER"/buzzer-support.cfg + if grep -q "include Helper-Script/buzzer-support" "$PRINTER_CFG" ; then + echo -e "Info: Removing Buzzer Support configurations in printer.cfg file..." + sed -i '/include Helper-Script\/buzzer-support\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Buzzer Support configurations are already removed in printer.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Buzzer Support has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/camera_settings_control.sh b/scripts/camera_settings_control.sh new file mode 100755 index 0000000..a490152 --- /dev/null +++ b/scripts/camera_settings_control.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +set -e + +function camera_settings_control_message(){ + top_line + title 'Camera Settings Control' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to install needed macros to adjust camera ${white}│" + echo -e " │ ${cyan}settings like brightness, saturation, contrast, etc... ${white}│" + hr + bottom_line +} + +function install_camera_settings_control(){ + camera_settings_control_message + local yn + while true; do + install_msg "Camera Settings Control" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/camera-settings.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/camera-settings.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Linking file..." + cp "$CAMERA_SETTINGS_URL" "$HS_CONFIG_FOLDER"/camera-settings.cfg + if grep -q "include Helper-Script/camera-settings" "$PRINTER_CFG" ; then + echo -e "Info: Camera Settings configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Camera Settings configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/camera-settings\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Camera Settings Control has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_camera_settings_control(){ + camera_settings_control_message + local yn + while true; do + remove_msg "Camera Settings Control" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing file..." + "$HS_CONFIG_FOLDER"/camera-settings.cfg + if grep -q "include Helper-Script/camera-settings" "$PRINTER_CFG" ; then + echo -e "Info: Removing Camera Settings configurations in printer.cfg file..." + sed -i '/include Helper-Script\/camera-settings\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Camera Settings configurations are already removed in printer.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Camera Settings Control has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/creality_dynamic_logos.sh b/scripts/creality_dynamic_logos.sh new file mode 100755 index 0000000..58a2b97 --- /dev/null +++ b/scripts/creality_dynamic_logos.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +set -e + +function creality_dynamic_logos_message(){ + top_line + title 'Creality Dynamic Logos for Fluidd' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to have the dynamic Creality logos on the Fluidd ${white}│" + echo -e " │ ${cyan}Web interface. ${white}│" + hr + bottom_line +} + +function install_creality_dynamic_logos(){ + creality_dynamic_logos_message + local yn + while true; do + install_msg "Creality Dynamic Logos for Fluidd" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Copying files..." + cp "$FLUIDD_LOGO_URL1" "$FLUIDD_FOLDER"/logo_creality_v1.svg + cp "$FLUIDD_LOGO_URL2" "$FLUIDD_FOLDER"/logo_creality_v2.svg + rm -f "$FLUIDD_FOLDER"/config.json + cp "$FLUIDD_LOGO_URL3" "$FLUIDD_FOLDER"/config.json + ok_msg "Creality Dynamic Logos for Fluidd have been installed successfully!" + echo -e " You can now select ${yellow}Creality V1 ${white}or ${yellow}Creality V2 ${white}theme in Fluidd settings." + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/creality_web_interface.sh b/scripts/creality_web_interface.sh new file mode 100755 index 0000000..f8cefe7 --- /dev/null +++ b/scripts/creality_web_interface.sh @@ -0,0 +1,138 @@ +#!/bin/sh + +set -e + +function remove_creality_web_interface_message(){ + top_line + title 'Remove Creality Web Interface' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to remove Creality Web Interface and replace ${white}│" + echo -e " │ ${cyan}it with Fluidd or Mainsail on port 80. ${white}│" + hr + bottom_line +} + +function restore_creality_web_interface_message(){ + top_line + title 'Restore Creality Web Interface' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to restore Creality Web Interface on port 80. ${white}│" + hr + bottom_line +} + +function remove_creality_web_interface(){ + remove_creality_web_interface_message + local yn + while true; do + remove_msg "Creality Web Interface" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Disabling files..." + if [ -f /usr/bin/web-server ]; then + mv /usr/bin/web-server /usr/bin/web-server.disabled + fi + if [ -f /usr/bin/Monitor ]; then + mv /usr/bin/Monitor /usr/bin/Monitor.disabled + fi + echo -e "Info: Stopping services..." + set +e + killall -q Monitor + killall -q web-server + set -e + echo + if [ -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + echo -e "Info: Applying changes..." + sed -i '/listen 4408 default_server;/a \ listen 80;' /usr/data/nginx/nginx/nginx.conf + echo -e "Info: Restarting Nginx service..." + restart_nginx + ok_msg "Creality Web Interface has been removed successfully!" + echo -e " ${white}You can now connect to Fluidd Web Interface with ${yellow}http://$(check_ipaddress)${white}" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ -d "$FLUIDD_FOLDER" ]; then + echo -e "Info: Applying changes..." + sed -i '/listen 4409 default_server;/a \ listen 80;' /usr/data/nginx/nginx/nginx.conf + echo -e "Info: Restarting Nginx service..." + restart_nginx + ok_msg "Creality Web Interface has been removed successfully!" + echo -e " ${white}You can now connect to Mainsail Web Interface with ${yellow}http://$(check_ipaddress)${white}" + elif [ -d "$FLUIDD_FOLDER" ] && [ -d "$FLUIDD_FOLDER" ]; then + local interface_choice + while true; do + read -p " ${white}Which Web Interface do you want to set as default (on port 80)? (${yellow}fluidd${white}/${yellow}mainsail${white}): ${yellow}" interface_choice + case "${interface_choice}" in + FLUIDD|fluidd) + echo -e "${white}" + echo -e "Info: Applying changes..." + sed -i '/listen 4408 default_server;/a \ listen 80;' /usr/data/nginx/nginx/nginx.conf + echo -e "Info: Restarting Nginx service..." + restart_nginx + ok_msg "Creality Web Interface has been removed successfully!" + echo -e " You can now connect to Fluidd Web Interface with ${yellow}http://$(check_ipaddress) ${white}or ${yellow}http://$(check_ipaddress):4408${white}" + break;; + MAINSAIL|mainsail) + echo -e "${white}" + echo -e "Info: Applying changes..." + sed -i '/listen 4409 default_server;/a \ listen 80;' /usr/data/nginx/nginx/nginx.conf + echo -e "Info: Restarting Nginx service..." + restart_nginx + ok_msg "Creality Web Interface has been removed successfully!" + echo -e " You can now connect to Mainsail Web Interface with ${yellow}http://$(check_ipaddress) ${white}or ${yellow}http://$(check_ipaddress):4409${white}" + break;; + *) + error_msg "Please select a correct choice!";; + esac + done + fi + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function restore_creality_web_interface(){ + restore_creality_web_interface_message + local yn + while true; do + restore_msg "Creality Web Interface" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Restoring files..." + if [ -f /usr/bin/web-server.disabled ] && [ -f "$INITD_FOLDER"/S99start_app ]; then + mv /usr/bin/web-server.disabled /usr/bin/web-server + fi + if [ -f /usr/bin/Monitor.disabled ] && [ ! -d "$GUPPY_SCREEN_FOLDER" ]; then + mv /usr/bin/Monitor.disabled /usr/bin/Monitor + fi + echo -e "Info: Restoring changes..." + sed -i '/listen 80;/d' /usr/data/nginx/nginx/nginx.conf + echo -e "Info: Restarting services..." + restart_nginx + set +e + killall -q Monitor + killall -q web-server + set -e + if [ -f /usr/bin/web-server.disabled ] && [ -f "$INITD_FOLDER"/S99start_app ]; then + /usr/bin/web-server > /dev/null 2>&1 & + fi + if [ -f /usr/bin/Monitor.disabled ] && [ ! -d "$GUPPY_SCREEN_FOLDER" ]; then + /usr/bin/Monitor > /dev/null 2>&1 & + fi + ok_msg "Creality Web Interface has been restored successfully!" + echo -e " You can now connect to Creality Web Interface with ${yellow}http://$(check_ipaddress) ${white}and with ${yellow}Creality Print${white}." + return;; + N|n) + error_msg "Restoration canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/custom_boot_display.sh b/scripts/custom_boot_display.sh new file mode 100755 index 0000000..0fbabd7 --- /dev/null +++ b/scripts/custom_boot_display.sh @@ -0,0 +1,91 @@ +#!/bin/sh + +set -e + +function install_custom_boot_display_message(){ + top_line + title 'Install Custom Boot Display' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to install a custom Creality-themed boot ${white}│" + echo -e " │ ${cyan}display. ${white}│" + hr + bottom_line +} + +function remove_custom_boot_display_message(){ + top_line + title 'Remove Custom Boot Display' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to restore stock boot display. ${white}│" + hr + bottom_line +} + +function install_custom_boot_display(){ + install_custom_boot_display_message + local yn + while true; do + install_msg "Custom Boot Display" yn + case "${yn}" in + Y|y) + echo -e "${white}" + local printer_choice + while true; do + read -p " ${white}Do you want install it for ${yellow}K1${white} or ${yellow}K1 Max${white}? (${yellow}k1${white}/${yellow}k1max${white}): ${yellow}" printer_choice + case "${printer_choice}" in + K1|k1) + echo -e "${white}" + echo -e "Info: Removing stock files..." + rm -rf "$BOOT_DISPLAY_FOLDER"/part0 + rm -f "$BOOT_DISPLAY_FOLDER"/boot-display.conf + echo -e "Info: Extracting custom files..." + tar -xvf "$BOOT_DISPLAY_K1_URL" -C "$BOOT_DISPLAY_FOLDER" + break;; + K1MAX|k1max) + echo -e "${white}" + echo -e "Info: Removing stock files..." + rm -rf "$BOOT_DISPLAY_FOLDER"/part0 + rm -f "$BOOT_DISPLAY_FOLDER"/boot-display.conf + echo -e "Info: Extracting custom files..." + tar -xvf "$BOOT_DISPLAY_K1M_URL" -C "$BOOT_DISPLAY_FOLDER" + break;; + *) + error_msg "Please select a correct choice!";; + esac + done + ok_msg "Custom Boot Display has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_custom_boot_display(){ + remove_custom_boot_display_message + local yn + while true; do + remove_msg "Custom Boot Display" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing custom files..." + rm -rf "$BOOT_DISPLAY_FOLDER"/part0 + rm -f "$BOOT_DISPLAY_FOLDER"/boot-display.conf + echo -e "Info: Extracting stock files..." + tar -xvf "$BOOT_DISPLAY_STOCK_URL" -C "$BOOT_DISPLAY_FOLDER" + ok_msg "Custom Boot Display has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/entware.sh b/scripts/entware.sh new file mode 100755 index 0000000..3046e7e --- /dev/null +++ b/scripts/entware.sh @@ -0,0 +1,72 @@ +#!/bin/sh + +set -e + +function entware_message(){ + top_line + title 'Entware' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Entware is a software repository for devices which use Linux ${white}│" + echo -e " │ ${cyan}kernel. It allows packages to be added to your printer. ${white}│" + hr + bottom_line +} + +function install_entware(){ + entware_message + local yn + while true; do + install_msg "Entware" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Running Entware installer..." + set +e + chmod 755 "$ENTWARE_URL" + sh "$ENTWARE_URL" + set -e + ok_msg "Entware has been installed successfully!" + echo -e " Disconnect and reconnect SSH session, and you can now install packages with: ${yellow}opkg install ${white}" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_entware(){ + entware_message + local yn + while true; do + remove_msg "Entware" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing startup script..." + rm -f /etc/init.d/S50unslung + echo -e "Info: Removing directories..." + rm -rf /usr/data/opt + if [ -L /opt ]; then + rm /opt + mkdir -p /opt + chmod 755 /opt + fi + echo -e "Info: Removing SFTP server symlink..." + [ -L /usr/libexec/sftp-server ] && rm /usr/libexec/sftp-server + echo -e "Info: Removing changes in system profile..." + rm -f /etc/profile.d/entware.sh + sed -i 's/\/opt\/bin:\/opt\/sbin:\/bin:/\/bin:/' /etc/profile + ok_msg "Entware has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/fans_control_macros.sh b/scripts/fans_control_macros.sh new file mode 100755 index 0000000..9f50864 --- /dev/null +++ b/scripts/fans_control_macros.sh @@ -0,0 +1,129 @@ +#!/bin/sh + +set -e + +function fans_control_macros_message(){ + top_line + title 'Fans Control Macros' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to control Motherboard fan from Web interfaces ${white}│" + echo -e " │ ${cyan}or with slicers. ${white}│" + hr + bottom_line +} + +function install_fans_control_macros(){ + fans_control_macros_message + local yn + while true; do + install_msg "Fans Control Macros" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/fans-control.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/fans-control.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Linking file..." + ln -sf "$FAN_CONTROLS_URL" "$HS_CONFIG_FOLDER"/fans-control.cfg + if grep -q "include Helper-Script/fans-control" "$PRINTER_CFG" ; then + echo -e "Info: Fans Control Macros configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Fans Control Macros configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/fans-control\.cfg\]' "$PRINTER_CFG" + fi + if grep -q "\[duplicate_pin_override\]" "$PRINTER_CFG" ; then + echo -e "Info: Disabling [duplicate_pin_override] configuration in printer.cfg file..." + sed -i '/^\[duplicate_pin_override\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$PRINTER_CFG" + else + echo -e "Info: [duplicate_pin_override] configuration is already disabled in printer.cfg file..." + fi + if grep -q "\[temperature_fan chamber_fan\]" "$PRINTER_CFG" ; then + echo -e "Info: Disabling [temperature_fan chamber_fan] configuration in printer.cfg file..." + sed -i '/^\[temperature_fan chamber_fan\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$PRINTER_CFG" + else + echo -e "Info: [temperature_fan chamber_fan] configuration is already disabled in printer.cfg file..." + fi + if grep -q "\[gcode_macro M106\]" "$MACROS_CFG" ; then + echo -e "Info: Disabling [gcode_macro M106] in gcode_macro.cfg file..." + sed -i '/^\[gcode_macro M106\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro M106] macro is already disabled in gcode_macro.cfg file..." + fi + if grep -q "\[gcode_macro M141\]" "$MACROS_CFG" ; then + echo -e "Info: Disabling [gcode_macro M141] in gcode_macro.cfg file..." + sed -i '/^\[gcode_macro M141\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro M141] macro is already disabled in gcode_macro.cfg file..." + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Fans Control Macros have been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_fans_control_macros(){ + fans_control_macros_message + local yn + while true; do + remove_msg "Fans Control Macros" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing file..." + rm -f "$HS_CONFIG_FOLDER"/fans-control.cfg + if grep -q "include Helper-Script/fans-control" "$PRINTER_CFG" ; then + echo -e "Info: Removing Fans Control Macros configurations in printer.cfg file..." + sed -i '/include Helper-Script\/fans-control\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Fans Control Macros configurations are already removed in printer.cfg file..." + fi + if grep -q "#\[duplicate_pin_override\]" "$PRINTER_CFG" ; then + echo -e "Info: Enabling [duplicate_pin_override] in printer.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[duplicate_pin_override\]/[duplicate_pin_override]/' -e '/^\[duplicate_pin_override\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$PRINTER_CFG" + else + echo -e "Info: [duplicate_pin_override] is already enabled in printer.cfg file..." + fi + if grep -q "#\[temperature_fan chamber_fan\]" "$PRINTER_CFG" ; then + echo -e "Info: Enabling [temperature_fan chamber_fan] in printer.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[temperature_fan chamber_fan\]/[temperature_fan chamber_fan]/' -e '/^\[temperature_fan chamber_fan\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$PRINTER_CFG" + else + echo -e "Info: [temperature_fan chamber_fan] is already enabled in printer.cfg file..." + fi + if grep -q "#\[gcode_macro M106\]" "$MACROS_CFG" ; then + echo -e "Info: Enabling [gcode_macro M106] in gcode_macro.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[gcode_macro M106\]/[gcode_macro M106]/' -e '/^\[gcode_macro M106\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro M106] is already enabled in gcode_macro.cfg file..." + fi + if grep -q "#\[gcode_macro M141\]" "$MACROS_CFG" ; then + echo -e "Info: Enabling [gcode_macro M141] in gcode_macro.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[gcode_macro M141\]/[gcode_macro M141]/' -e '/^\[gcode_macro M141\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro M141] is already enabled in gcode_macro.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Fans Control Macros have been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/fluidd.sh b/scripts/fluidd.sh new file mode 100755 index 0000000..3189e36 --- /dev/null +++ b/scripts/fluidd.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +set -e + +function fluidd_message(){ + top_line + title 'Fluidd' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Fluidd is a free and open-source Klipper Web interface for ${white}│" + echo -e " │ ${cyan}managing your 3d printer. ${white}│" + echo -e " │ ${cyan}It will be accessible on port 4408. ${white}│" + hr + bottom_line +} + +function install_fluidd(){ + fluidd_message + local yn + while true; do + install_msg "Fluidd" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Downloading Fluidd file..." + "$CURL" -L "$FLUIDD_URL" -o "$USR_DATA"/fluidd.zip + echo -e "Info: Creating directory..." + if [ -d "$FLUIDD_FOLDER" ]; then + rm -rf "$FLUIDD_FOLDER" + fi + mkdir -p "$FLUIDD_FOLDER" + mv "$USR_DATA"/fluidd.zip "$FLUIDD_FOLDER" + echo -e "Info: Extracting files..." + unzip "$FLUIDD_FOLDER"/fluidd.zip -d "$FLUIDD_FOLDER" + echo -e "Info: Removing file..." + rm -f "$FLUIDD_FOLDER"/fluidd.zip + if grep -q "#\[update_manager fluidd\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Enabling Fluidd configurations for Update Manager..." + sed -i -e 's/^\s*#[[:space:]]*\[update_manager fluidd\]/[update_manager fluidd]/' -e '/^\[update_manager fluidd\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MOONRAKER_CFG" + else + echo -e "Info: Fluidd configurations are already enabled for Update Manager..." + fi + echo -e "Info: Retarting Nginx service..." + restart_nginx + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + ok_msg "Fluidd has been installed successfully!" + echo -e " You can now connect to Fluidd Web Interface with ${yellow}http://$(check_ipaddress):4408${white}" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_fluidd(){ + fluidd_message + local yn + while true; do + remove_msg "Fluidd" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing files..." + rm -rf "$FLUIDD_FOLDER" + if grep -q "\[update_manager fluidd\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Disabling Fluidd configurations for Update Manager..." + sed -i '/^\[update_manager fluidd\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MOONRAKER_CFG" + echo -e "Info: Retarting Nginx service..." + restart_nginx + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + else + echo -e "Info: Fluidd configurations are already disabled for Update Manager..." + fi + ok_msg "Fluidd has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/gcode_shell_command.sh b/scripts/gcode_shell_command.sh new file mode 100755 index 0000000..54cde4a --- /dev/null +++ b/scripts/gcode_shell_command.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +set -e + +function gcode_shell_command_message(){ + top_line + title 'Klipper Gcode Shell Command' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}After installing this extension you can execute Linux ${white}│" + echo -e " │ ${cyan}commands or even scripts from Klipper with custom commands ${white}│" + echo -e " │ ${cyan}defined in your configuration files. ${white}│" + hr + bottom_line +} + +function install_gcode_shell_command(){ + gcode_shell_command_message + local yn + while true; do + install_msg "Klipper Gcode Shell Command" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Linking file..." + ln -sf "$KLIPPER_SHELL_URL" "$KLIPPER_EXTRAS_FOLDER"/gcode_shell_command.py + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Klipper Gcode Shell Command has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_gcode_shell_command(){ + gcode_shell_command_message + local yn + while true; do + remove_msg "Klipper Gcode Shell Command" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing files..." + rm -f "$KLIPPER_EXTRAS_FOLDER"/gcode_shell_command.py + rm -f "$KLIPPER_EXTRAS_FOLDER"/gcode_shell_command.pyc + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Klipper Gcode Shell Command has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/git_backup.sh b/scripts/git_backup.sh new file mode 100755 index 0000000..0325ace --- /dev/null +++ b/scripts/git_backup.sh @@ -0,0 +1,91 @@ +#!/bin/sh + +set -e + +function git_backup_message(){ + top_line + title 'Git Backup' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to watch Klipper configuration folder and ${white}│" + echo -e " │ ${cyan}automatically backup to GitHub whenever a change is made in ${white}│" + echo -e " │ ${cyan}that directory. ${white}│" + hr + bottom_line +} + +function install_git_backup(){ + git_backup_message + local yn + while true; do + install_msg "Git Backup" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/git-backup.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/git-backup.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Running Git Backup installer..." + chmod 755 "$GIT_BACKUP_INSTALLER" + sh "$GIT_BACKUP_INSTALLER" -i + echo -e "Info: Linking file..." + ln -sf "$GIT_BACKUP_URL"/git-backup.cfg "$HS_CONFIG_FOLDER"/git-backup.cfg + if grep -q "include Helper-Script/git-backup" "$PRINTER_CFG" ; then + echo -e "Info: Git Backup configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Git Backup configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/git-backup\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Git Backup has been installed and configured successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_git_backup(){ + git_backup_message + local yn + while true; do + remove_msg "Git Backup" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing files..." + rm -f "$HS_CONFIG_FOLDER"/git-backup.cfg + rm -f "INITD_FOLDER"/S52Git-Backup + if grep -q "include Helper-Script/git-backup" "$PRINTER_CFG" ; then + echo -e "Info: Removing Git Backup configurations in printer.cfg file..." + sed -i '/include Helper-Script\/git-backup\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Git Backup configurations are already removed in printer.cfg file..." + fi + if [ -f "$ENTWARE_FILE" ]; then + echo -e "Info: Removing packages..." + "$ENTWARE_FILE" --autoremove remove inotifywait + "$ENTWARE_FILE" --autoremove remove procps-ng-pkill + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Git Backup has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/guppy_screen.sh b/scripts/guppy_screen.sh new file mode 100755 index 0000000..198adaf --- /dev/null +++ b/scripts/guppy_screen.sh @@ -0,0 +1,250 @@ +#!/bin/sh + +set -e + +function guppy_screen_message(){ + top_line + title 'Guppy Screen' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Guppy Screen is a touch UI for Klipper using APIs exposed by ${white}│" + echo -e " │ ${cyan}Moonraker. It replace Creality touch UI. ${white}│" + hr + bottom_line +} + +function install_guppy_screen(){ + guppy_screen_message + local yn + while true; do + install_msg "Guppy Screen" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$USR_DATA"/guppyscreen.tar.gz ]; then + rm -f "$USR_DATA"/guppyscreen.tar.gz + fi + if [ $K1 -eq 1 ]; then + local theme_choice + while true; do + read -p " Do you want to install it with ${green}Material Design ${white}or ${green}Z-Bolt ${white}theme? (${yellow}material${white}/${yellow}zbolt${white}): ${yellow}" theme_choice + case "${theme_choice}" in + MATERIAL|material) + echo -e "${white}" + echo -e "Info: Downloading Guppy Screen..." + "$CURL" -L https://github.com/ballaswag/guppyscreen/releases/latest/download/guppyscreen.tar.gz -o "$USR_DATA"/guppyscreen.tar.gz + break;; + ZBOLT|zbolt) + echo -e "${white}" + echo -e "Info: Downloading Guppy Screen..." + "$CURL" -L https://github.com/ballaswag/guppyscreen/releases/latest/download/guppyscreen-zbolt.tar.gz -o "$USR_DATA"/guppyscreen.tar.gz + break;; + *) + error_msg "Please select a correct choice!";; + esac + done + else + echo -e "Info: Downloading Guppy Screen..." + "$CURL" -L https://github.com/ballaswag/guppyscreen/releases/latest/download/guppyscreen-smallscreen.tar.gz -o "$USR_DATA"/guppyscreen.tar.gz + fi + echo -e "Info: Installing files..." + tar -xvf "$USR_DATA"/guppyscreen.tar.gz -C "$USR_DATA" + rm -f "$USR_DATA"/guppyscreen.tar.gz + if [ ! -d "$HS_BACKUP_FOLDER"/guppyscreen ]; then + echo -e "Info: Backing up original file..." + mkdir -p "$HS_BACKUP_FOLDER"/guppyscreen + mv "$INITD_FOLDER"/S12boot_display "$HS_BACKUP_FOLDER"/guppyscreen + cp "$INITD_FOLDER"/S50dropbear "$HS_BACKUP_FOLDER"/guppyscreen + cp "$INITD_FOLDER"/S99start_app "$HS_BACKUP_FOLDER"/guppyscreen + fi + if [ ! -f "$HS_BACKUP_FOLDER"/guppyscreen/ft2font.cpython-38-mipsel-linux-gnu.so ]; then + mv /usr/lib/python3.8/site-packages/matplotlib/ft2font.cpython-38-mipsel-linux-gnu.so "$HS_BACKUP_FOLDER"/guppyscreen + fi + local yn + while true; do + echo + echo -e " ${white}Do you want to disable all Creality services ?" + echo -e " ${yellow}Benefits: ${white}\e[97mFrees up system resources on your K1 for critical services such as Klipper (recommended)${white}" + echo -e " ${yellow}Disadvantages: ${white}\e[97mDisabling all Creality services breaks Creality Cloud and Creality Print${white}" + echo + read -p " Do you want to disable all Creality Services? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Disabling Creality services..." + rm -f "$INITD_FOLDER"/S99start_app + set +e + killall -q master-server + killall -q audio-server + killall -q wifi-server + killall -q app-server + killall -q upgrade-server + killall -q web-server + set -e + break;; + N|n) + break;; + *) + error_msg "Please select a correct choice!";; + esac + done + if [ ! -d "/usr/lib/python3.8/site-packages/matplotlib-2.2.3-py3.8.egg-info" ]; then + echo -e "Info: mathplotlib ft2font module is not replaced. PSD graphs might not work..." + else + echo -e "Info: Replacing mathplotlib ft2font module to generate PSD graphs..." + cp "$GUPPY_SCREEN_FOLDER"/k1_mods/ft2font.cpython-38-mipsel-linux-gnu.so /usr/lib/python3.8/site-packages/matplotlib/ft2font.cpython-38-mipsel-linux-gnu.so + fi + echo -e "Info: Setting up Guppy Screen..." + cp "$GUPPY_SCREEN_FOLDER"/k1_mods/S50dropbear "$INITD_FOLDER"/S50dropbear + cp "$GUPPY_SCREEN_FOLDER"/k1_mods/S99guppyscreen "$INITD_FOLDER"/S99guppyscreen + ln -sf "$GUPPY_SCREEN_FOLDER"/k1_mods/calibrate_shaper_config.py "$KLIPPER_EXTRAS_FOLDER"/calibrate_shaper_config.py + ln -sf "$GUPPY_SCREEN_FOLDER"/k1_mods/guppy_module_loader.py "$KLIPPER_EXTRAS_FOLDER"/guppy_module_loader.py + ln -sf "$GUPPY_SCREEN_FOLDER"/k1_mods/guppy_config_helper.py "$KLIPPER_EXTRAS_FOLDER"/guppy_config_helper.py + ln -sf "$GUPPY_SCREEN_FOLDER"/k1_mods/tmcstatus.py "$KLIPPER_EXTRAS_FOLDER"/tmcstatus.py + ln -sf "$GUPPY_SCREEN_FOLDER"/k1_mods/respawn/libeinfo.so.1 /lib/libeinfo.so.1 + ln -sf "$GUPPY_SCREEN_FOLDER"/k1_mods/respawn/librc.so.1 /lib/librc.so.1 + mkdir -p "$KLIPPER_CONFIG_FOLDER"/GuppyScreen/scripts + cp "$GUPPY_SCREEN_FOLDER"/scripts/*.cfg "$KLIPPER_CONFIG_FOLDER"/GuppyScreen + cp "$GUPPY_SCREEN_FOLDER"/scripts/*.py "$KLIPPER_CONFIG_FOLDER"/GuppyScreen/scripts + ln -sf "$GUPPY_SCREEN_URL1" "$KLIPPER_CONFIG_FOLDER"/GuppyScreen/guppy_update.cfg + chmod 775 "$GUPPY_SCREEN_URL2" + if grep -q "include GuppyScreen" "$PRINTER_CFG" ; then + echo -e "Info: Guppy Screen configurations are already enabled in printer.cfg file." + else + echo -e "Info: Adding Guppy Screen configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include GuppyScreen/*\.cfg\]' "$PRINTER_CFG" + fi + if grep -q 'variable_autotune_shapers:' "$MACROS_CFG" ; then + echo -e "Info: Disabling stock configuration in gcode_macro.cfg file..." + sed -i 's/variable_autotune_shapers:/#&/' "$MACROS_CFG" + else + echo -e "Info: Stock configuration is already disabled in gcode_macro.cfg file..." + fi + if grep -q '\[gcode_macro INPUTSHAPER\]' "$MACROS_CFG" ; then + echo -e "Info: Replacing stock configuration in gcode_macro.cfg file..." + sed -i 's/SHAPER_CALIBRATE AXIS=y/SHAPER_CALIBRATE/' "$MACROS_CFG" + else + echo -e "Info: Stock configuration is already replaced in gcode_macro.cfg file..." + fi + sync + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + echo -e "Info: Restarting Klipper service..." + restart_klipper + echo -e "Info: Disabling services..." + if [ -f /usr/bin/Monitor ]; then + mv /usr/bin/Monitor /usr/bin/Monitor.disable + fi + if [ -f /usr/bin/display-server ]; then + mv /usr/bin/display-server /usr/bin/display-server.disable + fi + set +e + killall -q Monitor + killall -q display-server + set -e + echo -e "Info: Starting Guppy Screen service..." + /etc/init.d/S99guppyscreen restart &> /dev/null + sleep 1 + ps auxw | grep guppyscreen | grep -v sh | grep -v grep + ok_msg "Guppy Screen has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_guppy_screen(){ + guppy_screen_message + local yn + while true; do + remove_msg "Guppy Screen" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Restoring backup files..." + if [ -d "$HS_BACKUP_FOLDER"/guppyscreen ]; then + cp "$HS_BACKUP_FOLDER"/guppyscreen/S12boot_display "$INITD_FOLDER"/S12boot_display + cp "$HS_BACKUP_FOLDER"/guppyscreen/S50dropbear "$INITD_FOLDER"/S50dropbear + cp "$HS_BACKUP_FOLDER"/guppyscreen/S99start_app "$INITD_FOLDER"/S99start_app + rm -rf "$HS_BACKUP_FOLDER"/guppyscreen + fi + if [ ! -n "$(ls -A "$HS_BACKUP_FOLDER")" ]; then + rm -rf "$HS_BACKUP_FOLDER" + fi + echo -e "Info: Stopping Guppy Screen Service..." + [ -f "$INITD_FOLDER"/S99guppyscreen ] && "$INITD_FOLDER"/S99guppyscreen stop &> /dev/null + set +e + killall -q guppyscreen + set -e + echo -e "Info: Removing files..." + rm -f "$KLIPPER_EXTRAS_FOLDER"/calibrate_shaper_config.py + rm -f "$KLIPPER_EXTRAS_FOLDER"/calibrate_shaper_config.pyc + rm -f "$KLIPPER_EXTRAS_FOLDER"/guppy_module_loader.py + rm -f "$KLIPPER_EXTRAS_FOLDER"/guppy_module_loader.pyc + rm -f "$KLIPPER_EXTRAS_FOLDER"/guppy_config_helper.py + rm -f "$KLIPPER_EXTRAS_FOLDER"/guppy_config_helper.pyc + rm -f "$KLIPPER_EXTRAS_FOLDER"/tmcstatus.py + rm -f "$KLIPPER_EXTRAS_FOLDER"/tmcstatus.pyc + rm -f "$INITD_FOLDER"/S99guppyscreen + rm -f /lib/libeinfo.so.1 + rm -f /lib/librc.so.1 + rm -rf "$GUPPY_SCREEN_FOLDER" + rm -rf "$KLIPPER_CONFIG_FOLDER"/GuppyScreen + if grep -q "include GuppyScreen/*" "$PRINTER_CFG" ; then + echo -e "Info: Removing Guppy Screen configurations in printer.cfg file..." + sed -i '/\[include GuppyScreen\/\*\.cfg\]/d' "$PRINTER_CFG" + else + echo -e "Info: Guppy Screen configurations are already removed in printer.cfg file..." + fi + if grep -q "#variable_autotune_shapers:" "$MACROS_CFG"; then + echo -e "Info: Enabling stock configuration in gcode_macro.cfg file..." + sed -i 's/#variable_autotune_shapers:/variable_autotune_shapers:/' "$MACROS_CFG" + else + echo -e "Info: Stock configuration is already enabled in gcode_macro.cfg file..." + fi + if grep -q '\[gcode_macro INPUTSHAPER\]' "$MACROS_CFG" ; then + echo -e "Info: Restoring stock configuration in gcode_macro.cfg file..." + sed -i 's/SHAPER_CALIBRATE/SHAPER_CALIBRATE AXIS=y/' "$MACROS_CFG" + else + echo -e "Info: Stock configuration is already restored in gcode_macro.cfg file..." + fi + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + echo -e "Info: Restarting Klipper service..." + restart_klipper + echo -e "Info: Restoring services..." + if [ -f /usr/bin/Monitor.disable ]; then + mv /usr/bin/Monitor.disable /usr/bin/Monitor + fi + if [ -f /usr/bin/display-server.disable ]; then + mv /usr/bin/display-server.disable /usr/bin/display-server + fi + echo -e "Info: Restarting Creality services..." + set +e + /usr/bin/Monitor > /dev/null 2>&1 & + /usr/bin/display-server > /dev/null 2>&1 & + /usr/bin/master-server > /dev/null 2>&1 & + /usr/bin/audio-server > /dev/null 2>&1 & + /usr/bin/wifi-server > /dev/null 2>&1 & + /usr/bin/app-server > /dev/null 2>&1 & + /usr/bin/upgrade-server > /dev/null 2>&1 & + if [ -f /usr/bin/web-server ]; then + /usr/bin/web-server > /dev/null 2>&1 & + fi + set -e + ok_msg "Guppy Screen has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/improved_shapers.sh b/scripts/improved_shapers.sh new file mode 100755 index 0000000..58389ee --- /dev/null +++ b/scripts/improved_shapers.sh @@ -0,0 +1,140 @@ +#!/bin/sh + +set -e + +function improved_shapers_message(){ + top_line + title 'Improved Shapers Calibrations' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to calibrate Input Shaper, Belts Tension and ${white}│" + echo -e " │ ${cyan}generate Graphs. ${white}│" + hr + bottom_line +} + +function install_improved_shapers(){ + improved_shapers_message + local yn + while true; do + install_msg "Improved Shapers Calibrations" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Backing up original file..." + if [ ! -d "$HS_BACKUP_FOLDER"/improved-shapers ]; then + mkdir -p "$HS_BACKUP_FOLDER"/improved-shapers + fi + if [ ! -f "$HS_BACKUP_FOLDER"/ft2font.cpython-38-mipsel-linux-gnu.so ]; then + mv /usr/lib/python3.8/site-packages/matplotlib/ft2font.cpython-38-mipsel-linux-gnu.so "$HS_BACKUP_FOLDER"/improved-shapers + fi + echo -e "Info: Linking files..." + ln -sf "$IMP_SHAPERS_URL"/calibrate_shaper_config.py "$KLIPPER_EXTRAS_FOLDER"/calibrate_shaper_config.py + if [ ! -d "/usr/lib/python3.8/site-packages/matplotlib-2.2.3-py3.8.egg-info" ]; then + echo -e "Info: mathplotlib ft2font module is not replaced. PSD graphs might not work..." + else + echo -e "Info: Replacing mathplotlib ft2font module to generate PSD graphs..." + cp "$IMP_SHAPERS_URL"/ft2font.cpython-38-mipsel-linux-gnu.so /usr/lib/python3.8/site-packages/matplotlib/ft2font.cpython-38-mipsel-linux-gnu.so + fi + if [ -f "$HS_CONFIG_FOLDER"/improved-shapers ]; then + rm -rf "$HS_CONFIG_FOLDER"/improved-shapers + fi + if [ ! -d "$HS_CONFIG_FOLDER"/improved-shapers/scripts ]; then + mkdir -p "$HS_CONFIG_FOLDER"/improved-shapers/scripts + fi + cp "$IMP_SHAPERS_URL"/scripts/*.py "$HS_CONFIG_FOLDER"/improved-shapers/scripts + ln -sf "$IMP_SHAPERS_URL"/improved-shapers.cfg "$HS_CONFIG_FOLDER"/improved-shapers/improved-shapers.cfg + if grep -q 'variable_autotune_shapers:' "$MACROS_CFG" ; then + echo -e "Info: Disabling [gcode_macro AUTOTUNE_SHAPERS] configurations in gcode_macro.cfg file..." + sed -i 's/variable_autotune_shapers:/#&/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro AUTOTUNE_SHAPERS] configurations are already disabled in gcode_macro.cfg file..." + fi + if [ $K1 -eq 1 ]; then + if grep -q '\[gcode_macro INPUTSHAPER\]' "$MACROS_CFG" ; then + echo -e "Info: Replacing [gcode_macro INPUTSHAPER] configurations in gcode_macro.cfg file..." + sed -i 's/SHAPER_CALIBRATE AXIS=y/SHAPER_CALIBRATE/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro INPUTSHAPER] configurations are already replaced in gcode_macro.cfg file..." + fi + fi + if grep -q "include Helper-Script/improved-shapers/improved-shapers" "$PRINTER_CFG" ; then + echo -e "Info: Improved Shapers Calibration configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Improved Shapers Calibration configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/improved-shapers/improved-shapers\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Improved Shapers Calibrations have been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_improved_shapers(){ + improved_shapers_message + local yn + while true; do + remove_msg "Improved Shapers Calibrations" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Restoring original file..." + if [ -f "$HS_BACKUP_FOLDER"/ft2font.cpython-38-mipsel-linux-gnu.so ]; then + mv "$HS_BACKUP_FOLDER"/improved-shapers/ft2font.cpython-38-mipsel-linux-gnu.so /usr/lib/python3.8/site-packages/matplotlib + rm -rf "$HS_BACKUP_FOLDER"/improved-shapers + fi + if [ ! -n "$(ls -A "$HS_BACKUP_FOLDER")" ]; then + rm -rf "$HS_BACKUP_FOLDER" + fi + echo -e "Info: Removing files..." + rm -rf "$IMP_SHAPERS_FOLDER" + rm -f "$KLIPPER_EXTRAS_FOLDER"/calibrate_shaper_config.py + rm -f "$KLIPPER_EXTRAS_FOLDER"/calibrate_shaper_config.pyc + if grep -q "#variable_autotune_shapers:" "$MACROS_CFG"; then + echo -e "Info: Restoring [gcode_macro AUTOTUNE_SHAPERS] configurations in gcode_macro.cfg file..." + sed -i 's/#variable_autotune_shapers:/variable_autotune_shapers:/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro AUTOTUNE_SHAPERS] configurations are already restored in gcode_macro.cfg file..." + fi + if [ $K1 -eq 1 ]; then + if grep -q '\[gcode_macro INPUTSHAPER\]' "$MACROS_CFG" ; then + echo -e "Info: Restoring [gcode_macro INPUTSHAPER] configurations in gcode_macro.cfg file..." + sed -i 's/SHAPER_CALIBRATE/SHAPER_CALIBRATE AXIS=y/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro INPUTSHAPER] configurations are already restored in gcode_macro.cfg file..." + fi + fi + if grep -q "include Helper-Script/improved-shapers/improved-shapers" "$PRINTER_CFG" ; then + echo -e "Info: Removing Improved Shapers Calibrations in printer.cfg file..." + sed -i '/include Helper-Script\/improved-shapers\/improved-shapers\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Improved Shapers Calibrations are already removed in printer.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Improved Shapers Calibrations have been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/kamp.sh b/scripts/kamp.sh new file mode 100755 index 0000000..eea113e --- /dev/null +++ b/scripts/kamp.sh @@ -0,0 +1,126 @@ +#!/bin/sh + +set -e + +function kamp_message(){ + top_line + title 'Klipper Adaptive Meshing & Purging' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}KAMP is an extension that allows to generate a mesh and ${white}│" + echo -e " │ ${cyan}purge line only in the area you really need it. ${white}│" + hr + bottom_line +} + +function install_kamp(){ + kamp_message + local yn + while true; do + install_msg "Klipper Adaptive Meshing & Purging" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -d "$HS_CONFIG_FOLDER"/KAMP ]; then + rm -rf "$HS_CONFIG_FOLDER"/KAMP + fi + if [ -f "$HS_CONFIG_FOLDER"/KAMP_Settings.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/KAMP_Settings.cfg + fi + echo -e "Info: Creating directories..." + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + mkdir -p "$HS_CONFIG_FOLDER"/KAMP + echo -e "Info: Linking files..." + ln -sf "$KAMP_URL"/Adaptive_Meshing.cfg "$KAMP_FOLDER"/Adaptive_Meshing.cfg + ln -sf "$KAMP_URL"/Line_Purge.cfg "$KAMP_FOLDER"/Line_Purge.cfg + ln -sf "$KAMP_URL"/Prusa_Slicer.cfg "$KAMP_FOLDER"/Prusa_Slicer.cfg + ln -sf "$KAMP_URL"/Smart_Park.cfg "$KAMP_FOLDER"/Smart_Park.cfg + ln -sf "$KAMP_URL"/Start_Print.cfg "$KAMP_FOLDER"/Start_Print.cfg + cp "$KAMP_URL"/KAMP_Settings.cfg "$KAMP_FOLDER"/KAMP_Settings.cfg + if grep -q "include Helper-Script/KAMP/KAMP_Settings" "$PRINTER_CFG" ; then + echo -e "Info: KAMP configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding KAMP configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a [include Helper-Script/KAMP/KAMP_Settings.cfg]' "$PRINTER_CFG" + fi + if grep -q "\[gcode_macro START_PRINT\]" "$MACROS_CFG" ; then + echo -e "Info: Disabling [gcode_macro START_PRINT] in gcode_macro.cfg file..." + sed -i '/\[gcode_macro START_PRINT\]/,/^\s*CX_PRINT_DRAW_ONE_LINE/ { /^\s*$/d }' "$MACROS_CFG" + sed -i '/^\[gcode_macro START_PRINT\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro START_PRINT] is already disabled in gcode_macro.cfg file..." + fi + echo + local yn_prusa + while true; do + read -p " Do you want to enable needed macros for PrusaSlicer? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn_prusa + case "${yn_prusa}" in + Y|y) + echo -e "${white}" + if grep -q "#\[include Prusa_Slicer.cfg\]" "$KAMP_FOLDER"/KAMP_Settings.cfg ; then + echo -e "Info: Enabling [include Prusa_Slicer.cfg] in KAMP_Settings.cfg file..." + sed -i 's/^#\[include Prusa_Slicer\.cfg\]/[include Prusa_Slicer.cfg]/' "$KAMP_FOLDER"/KAMP_Settings.cfg + else + echo -e "Info: [include Prusa_Slicer.cfg] is already enabled in KAMP_Settings.cfg file..." + fi + break;; + N|n) + echo -e "${white}" + break;; + *) + error_msg "Please select a correct choice!";; + esac + done + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Klipper Adaptive Meshing & Purging has been installed successfully!" + echo -e " Make sure Label Objects setting is enabled in your slicer." + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_kamp(){ + kamp_message + local yn + while true; do + remove_msg "Klipper Adaptive Meshing & Purging" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing files..." + rm -rf "$HS_CONFIG_FOLDER"/KAMP + if grep -q "include Helper-Script/KAMP/KAMP_Settings" "$PRINTER_CFG" ; then + echo -e "Info: Removing KAMP configurations in printer.cfg file..." + sed -i '/include Helper-Script\/KAMP\/KAMP_Settings\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: KAMP configurations are already removed in printer.cfg file..." + fi + if grep -q "#\[gcode_macro START_PRINT\]" "$MACROS_CFG" ; then + echo -e "Info: Enabling [gcode_macro START_PRINT] in gcode_macro.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[gcode_macro START_PRINT\]/[gcode_macro START_PRINT]/' -e '/^\[gcode_macro START_PRINT\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro START_PRINT] is already enabled in gcode_macro.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Klipper Adaptive Meshing & Purging has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/m600_support.sh b/scripts/m600_support.sh new file mode 100755 index 0000000..8f1c834 --- /dev/null +++ b/scripts/m600_support.sh @@ -0,0 +1,117 @@ +#!/bin/sh + +set -e + +function m600_support_message(){ + top_line + title 'M600 Support' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to use M600 command in your slicer to change ${white}│" + echo -e " │ ${cyan}filament. ${white}│" + hr + bottom_line +} + +function install_m600_support(){ + m600_support_message + local yn + while true; do + install_msg "M600 Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/M600-support.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/M600-support.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Linking file..." + ln -sf "$M600_SUPPORT_URL" "$HS_CONFIG_FOLDER"/M600-support.cfg + if grep -q "include Helper-Script/M600-support" "$PRINTER_CFG" ; then + echo -e "Info: M600 Support configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding M600 Support configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/M600-support\.cfg\]' "$PRINTER_CFG" + fi + if grep -q "\[idle_timeout\]" "$PRINTER_CFG" ; then + echo -e "Info: Disabling [idle_timeout] configurations in printer.cfg file..." + sed -i '/^\[idle_timeout\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$PRINTER_CFG" + else + echo -e "Info: [idle_timeout] configurations are already disabled in printer.cfg file..." + fi + if grep -q "\[filament_switch_sensor filament_sensor\]" "$PRINTER_CFG" ; then + echo -e "Info: Disabling [filament_switch_sensor] configurations in printer.cfg file..." + sed -i '/^\[filament_switch_sensor filament_sensor\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$PRINTER_CFG" + else + echo -e "Info: [filament_switch_sensor] configurations are already disabled in printer.cfg file..." + fi + if grep -q "\[gcode_macro RESUME\]" "$MACROS_CFG" ; then + echo -e "Info: Disabling [gcode_macro RESUME] in gcode_macro.cfg file..." + sed -i '/^\[gcode_macro RESUME\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro RESUME] is already disabled in gcode_macro.cfg file..." + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "M600 Support has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_m600_support(){ + m600_support_message + local yn + while true; do + remove_msg "M600 Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing file..." + rm -f "$HS_CONFIG_FOLDER"/M600-support.cfg + if grep -q "include Helper-Script/M600-support" "$PRINTER_CFG" ; then + echo -e "Info: Removing M600 Support configurations in printer.cfg file..." + sed -i '/include Helper-Script\/M600-support\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: M600 Support configurations are already removed in printer.cfg file..." + fi + if grep -q "#\[idle_timeout\]" "$PRINTER_CFG" ; then + echo -e "Info: Enabling [idle_timeout] configurations in printer.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[idle_timeout\]/[idle_timeout]/' -e '/^\[idle_timeout\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$PRINTER_CFG" + else + echo -e "Info: [idle_timeout] configurations are already enabled in printer.cfg file..." + fi + if grep -q "#\[filament_switch_sensor filament_sensor\]" "$PRINTER_CFG" ; then + echo -e "Info: Enabling [filament_switch_sensor] configurations in printer.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[filament_switch_sensor filament_sensor\]/[filament_switch_sensor filament_sensor]/' -e '/^\[filament_switch_sensor filament_sensor\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$PRINTER_CFG" + else + echo -e "Info: [filament_switch_sensor] configurations are already enabled in printer.cfg file..." + fi + if grep -q "#\[gcode_macro RESUME\]" "$MACROS_CFG" ; then + echo -e "Info: Enabling [gcode_macro RESUME] in gcode_macro.cfg file..." + sed -i -e 's/^\s*#[[:space:]]*\[gcode_macro RESUME\]/[gcode_macro RESUME]/' -e '/^\[gcode_macro RESUME\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MACROS_CFG" + else + echo -e "Info: [gcode_macro RESUME] is already enabled in gcode_macro.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "M600 Support has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/mainsail.sh b/scripts/mainsail.sh new file mode 100755 index 0000000..5ab4697 --- /dev/null +++ b/scripts/mainsail.sh @@ -0,0 +1,91 @@ +#!/bin/sh + +set -e + +function mainsail_message(){ + top_line + title 'Mainsail' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Mainsail makes Klipper more accessible by adding a ${white}│" + echo -e " │ ${cyan}lightweight, responsive web user interface, centred around ${white}│" + echo -e " │ ${cyan}an intuitive and consistent design philosophy. ${white}│" + echo -e " │ ${cyan}It will be accessible on port 4409. ${white}│" + hr + bottom_line +} + +function install_mainsail(){ + mainsail_message + local yn + while true; do + install_msg "Mainsail" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Downloading Mainsail file..." + "$CURL" -L "$MAINSAIL_URL" -o "$USR_DATA"/mainsail.zip + echo -e "Info: Creating directory..." + if [ -d "$MAINSAIL_FOLDER" ]; then + rm -rf "$MAINSAIL_FOLDER" + fi + mkdir -p "$MAINSAIL_FOLDER" + mv "$USR_DATA"/mainsail.zip "$MAINSAIL_FOLDER" + echo -e "Info: Extracting files..." + unzip "$MAINSAIL_FOLDER"/mainsail.zip -d "$MAINSAIL_FOLDER" + echo -e "Info: Removing file..." + rm -f "$MAINSAIL_FOLDER"/mainsail.zip + if grep -q "#\[update_manager mainsail\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Enabling Mainsail configurations for Update Manager..." + sed -i -e 's/^\s*#[[:space:]]*\[update_manager mainsail\]/[update_manager mainsail]/' -e '/^\[update_manager mainsail\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MOONRAKER_CFG" + else + echo -e "Info: Mainsail configurations are already enabled for Update Manager..." + fi + echo -e "Info: Retarting Nginx service..." + restart_nginx + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + ok_msg "Mainsail has been installed successfully!" + echo -e " You can now connect to Mainsail Web Interface with ${yellow}http://$(check_ipaddress):4409${white}" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_mainsail(){ + mainsail_message + local yn + while true; do + remove_msg "Mainsail" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing files..." + rm -rf "$MAINSAIL_FOLDER" + if grep -q "\[update_manager mainsail\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Disabling Mainsail configurations for Update Manager..." + sed -i '/^\[update_manager mainsail\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MOONRAKER_CFG" + echo -e "Info: Retarting Nginx service..." + restart_nginx + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + else + echo -e "Info: Mainsail configurations are already disabled for Update Manager..." + fi + ok_msg "Mainsail has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/menu/KE/customize_menu_KE.sh b/scripts/menu/KE/customize_menu_KE.sh new file mode 100755 index 0000000..fbfa913 --- /dev/null +++ b/scripts/menu/KE/customize_menu_KE.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +set -e + +function customize_menu_ui_ke() { + top_line + title '[ CUSTOMIZE MENU ]' "${yellow}" + inner_line + hr + menu_option '1' 'Remove' 'Creality Web Interface' + menu_option '2' 'Restore' 'Creality Web Interface' + hr + menu_option '3' 'Install' 'Guppy Screen' + menu_option '4' 'Remove' 'Guppy Screen' + hr + menu_option '5' 'Install' 'Creality Dynamic Logos for Fluidd' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function customize_menu_ke() { + clear + customize_menu_ui_ke + local customize_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" customize_menu_opt + case "${customize_menu_opt}" in + 1) + if [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install it first!" + elif [ ! -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is already removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to change the default Web Interface.${white}" + echo + else + run "remove_creality_web_interface" "customize_menu_ui_ke" + fi;; + 2) + if [ -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is already present!" + else + run "restore_creality_web_interface" "customize_menu_ui_ke" + fi;; + 3) + if [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen is already installed!" + echo -e " ${darkred}Please remove Guppy Screen first if you want to change the theme.${white}" + echo + elif [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Please remove Improved Shapers Calibrations first, Guppy Screen already use it!" + elif [ ! -f /lib/ld-2.29.so ]; then + error_msg "Make sure you're running 1.3.x.x firmware version!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_guppy_screen" "customize_menu_ui_ke" + fi;; + 4) + if [ ! -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen is not installed!" + else + run "remove_guppy_screen" "customize_menu_ui_ke" + fi;; + 5) + if [ -f "$FLUIDD_LOGO_FILE" ]; then + error_msg "Creality Dynamic Logos for Fluidd are already installed!" + elif [ ! -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is needed, please install it first!" + else + run "install_creality_dynamic_logos" "customize_menu_ui_ke" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + customize_menu_ke +} diff --git a/scripts/menu/KE/info_menu_KE.sh b/scripts/menu/KE/info_menu_KE.sh new file mode 100755 index 0000000..0b4b5dd --- /dev/null +++ b/scripts/menu/KE/info_menu_KE.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +set -e + +function check_folder_ke() { + local folder_path="$1" + if [ -d "$folder_path" ]; then + echo -e "${green}✓" + else + echo -e "${red}✗" + fi +} + +function check_file_ke() { + local file_path="$1" + if [ -f "$file_path" ]; then + echo -e "${green}✓" + else + echo -e "${red}✗" + fi +} + +function info_menu_ui_ke() { + top_line + title '[ INFORMATIONS MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + info_line "$(check_folder_ke "$MOONRAKER_FOLDER")" 'Moonraker & Nginx' + info_line "$(check_folder_ke "$FLUIDD_FOLDER")" 'Fluidd' + info_line "$(check_folder_ke "$MAINSAIL_FOLDER")" 'Mainsail' + hr + subtitle '•UTILITIES:' + info_line "$(check_file_ke "$ENTWARE_FILE")" 'Entware' + info_line "$(check_file_ke "$KLIPPER_SHELL_FILE")" 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + info_line "$(check_folder_ke "$IMP_SHAPERS_FOLDER")" 'Improved Shapers Calibrations' + info_line "$(check_file_ke "$SAVE_ZOFFSET_FILE")" 'Save Z-Offset Macros' + info_line "$(check_file_ke "$VIRTUAL_PINS_FILE")" 'Virtual Pins Support' + info_line "$(check_file_ke "$GIT_BACKUP_FILE")" 'Git Backup' + hr + subtitle '•CAMERA:' + info_line "$(check_file_ke "$TIMELAPSE_FILE")" 'Moonraker Timelapse' + hr + subtitle '•REMOTE ACCESS AND AI DETECTION:' + info_line "$(check_folder_ke "$OCTOEVERYWHERE_FOLDER")" 'OctoEverywhere' + info_line "$(check_folder_ke "$MOONRAKER_OBICO_FOLDER")" 'Obico' + info_line "$(check_folder_ke "$MOBILERAKER_COMPANION_FOLDER")" 'Mobileraker Companion' + hr + subtitle '•CUSTOMIZATION:' + info_line "$(check_file_ke "$CREALITY_WEB_FILE")" 'Creality Web Interface' + info_line "$(check_folder_ke "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' + info_line "$(check_file_ke "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function info_menu_ke() { + clear + info_menu_ui_ke + local info_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" info_menu_opt + case "${info_menu_opt}" in + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + info_menu_ke +} diff --git a/scripts/menu/KE/install_menu_KE.sh b/scripts/menu/KE/install_menu_KE.sh new file mode 100755 index 0000000..f33a08d --- /dev/null +++ b/scripts/menu/KE/install_menu_KE.sh @@ -0,0 +1,162 @@ +#!/bin/sh + +set -e + +function install_menu_ui_ke() { + top_line + title '[ INSTALL MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + menu_option ' 1' 'Install' 'Moonraker and Nginx' + menu_option ' 2' 'Install' 'Fluidd (port 4408)' + menu_option ' 3' 'Install' 'Mainsail (port 4409)' + hr + subtitle '•UTILITIES:' + menu_option ' 4' 'Install' 'Entware' + menu_option ' 5' 'Install' 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + menu_option ' 6' 'Install' 'Improved Shapers Calibrations' + menu_option ' 7' 'Install' 'Save Z-Offset Macros' + menu_option ' 8' 'Install' 'Virtual Pins Support' + menu_option ' 9' 'Install' 'Git Backup' + hr + subtitle '•CAMERA:' + menu_option '10' 'Install' 'Moonraker Timelapse' + hr + subtitle '•REMOTE ACCESS AND AI DETECTION:' + menu_option '11' 'Install' 'OctoEverywhere' + menu_option '12' 'Install' 'Moonraker Obico' + menu_option '13' 'Install' 'Mobileraker Companion' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function install_menu_ke() { + clear + install_menu_ui_ke + local install_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" install_menu_opt + case "${install_menu_opt}" in + 1) + if [ -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are already installed!" + else + run "install_moonraker_nginx" "install_menu_ui_ke" + fi;; + 2) + if [ -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + else + run "install_fluidd" "install_menu_ui_ke" + fi;; + 3) + if [ -d "$MAINSAIL_FOLDER" ]; then + error_msg "Mainsail is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + else + run "install_mainsail" "install_menu_ui_ke" + fi;; + 4) + if [ -f "$ENTWARE_FILE" ]; then + error_msg "Entware is already installed!" + else + run "install_entware" "install_menu_ui_ke" + fi;; + 5) + if [ -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is already installed!" + else + run "install_gcode_shell_command" "install_menu_ui_ke" + fi;; + 6) + if [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Improved Shapers Calibrations are already installed!" + elif [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen already has these features!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_improved_shapers" "install_menu_ui_ke" + fi;; + 7) + if [ -f "$SAVE_ZOFFSET_FILE" ]; then + error_msg "Save Z-Offset Macros are already installed!" + else + run "install_save_zoffset_macros" "install_menu_ui_ke" + fi;; + 8) + if [ -f "$VIRTUAL_PINS_FILE" ]; then + error_msg "Virtual Pins Support is already installed!" + else + run "install_virtual_pins" "install_menu_ui_ke" + fi;; + 9) + if [ -f "$GIT_BACKUP_FILE" ]; then + error_msg "Git Backup is already installed!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_git_backup" "install_menu_ui_ke" + fi;; + 10) + if [ -f "$TIMELAPSE_FILE" ]; then + error_msg "Moonraker Timelapse is already installed!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_moonraker_timelapse" "install_menu_ui_ke" + fi;; + 11) + if [ -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "OctoEverywhere is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install it first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_octoeverywhere" "install_menu_ui_ke" + fi;; + 12) + if [ -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Moonraker Obico is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install it first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_moonraker_obico" "install_menu_ui_ke" + fi;; + 13) + if [ -d "$MOBILERAKER_COMPANION_FOLDER" ]; then + error_msg "Mobileraker Companion is already installed!" + else + run "install_mobileraker_companion" "install_menu_ui_ke" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + install_menu_ke +} diff --git a/scripts/menu/KE/remove_menu_KE.sh b/scripts/menu/KE/remove_menu_KE.sh new file mode 100755 index 0000000..a82ef09 --- /dev/null +++ b/scripts/menu/KE/remove_menu_KE.sh @@ -0,0 +1,158 @@ +#!/bin/sh + +set -e + +function remove_menu_ui_ke() { + top_line + title '[ REMOVE MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + menu_option ' 1' 'Remove' 'Moonraker and Nginx' + menu_option ' 2' 'Remove' 'Fluidd (port 4408)' + menu_option ' 3' 'Remove' 'Mainsail (port 4409)' + hr + subtitle '•UTILITIES:' + menu_option ' 4' 'Remove' 'Entware' + menu_option ' 5' 'Remove' 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + menu_option ' 6' 'Remove' 'Improved Shapers Calibrations' + menu_option ' 7' 'Remove' 'Save Z-Offset Macros' + menu_option ' 8' 'Remove' 'Virtual Pins Support' + menu_option ' 9' 'Remove' 'Git Backup' + hr + subtitle '•CAMERA:' + menu_option '10' 'Remove' 'Moonraker Timelapse' + hr + subtitle '•REMOTE ACCESS AND AI DETECTION:' + menu_option '11' 'Remove' 'OctoEverywhere' + menu_option '12' 'Remove' 'Moonraker Obico' + menu_option '13' 'Remove' 'Mobileraker Companion' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function remove_menu_ke() { + clear + remove_menu_ui_ke + local remove_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" remove_menu_opt + case "${remove_menu_opt}" in + 1) + if [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are not installed!" + else + run "remove_moonraker_nginx" "remove_menu_ui_ke" + fi;; + 2) + if [ ! -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is not installed!" + elif [ ! -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to remove Fluidd.${white}" + echo + else + run "remove_fluidd" "remove_menu_ui_ke" + fi;; + 3) + if [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Mainsail is not installed!" + elif [ ! -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to remove Mainsail.${white}" + echo + else + run "remove_mainsail" "remove_menu_ui_ke" + fi;; + 4) + if [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is not installed!" + elif [ -f "$TIMELAPSE_FILE" ]; then + error_msg "Entware is needed to use Moonraker Timelapse, please uninstall it first!" + elif [ -f "$GIT_BACKUP_FILE" ]; then + error_msg "Entware is needed to use Git Backup, please uninstall it first!" + elif [ -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "Entware is needed to use OctoEverywhere, please uninstall it first!" + elif [ -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Entware is needed to use Moonraker Obico, please uninstall it first!" + else + run "remove_entware" "remove_menu_ui_ke" + fi;; + 5) + if [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is not installed!" + elif [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Guppy Screen, please uninstall it first!" + elif [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Improved Shapers Calibrations, please uninstall it first!" + elif [ -d "$GIT_BACKUP_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Git Backup, please uninstall it first!" + else + run "remove_gcode_shell_command" "remove_menu_ui_ke" + fi;; + 6) + if [ ! -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Improved Shapers Calibrations are not installed!" + else + run "remove_improved_shapers" "remove_menu_ui_ke" + fi;; + 7) + if [ ! -f "$SAVE_ZOFFSET_FILE" ]; then + error_msg "Save Z-Offset Macros are not installed!" + else + run "remove_save_zoffset_macros" "remove_menu_ui_ke" + fi;; + 8) + if [ ! -f "$VIRTUAL_PINS_FILE" ]; then + error_msg "Virtual Pins Support is not installed!" + else + run "remove_virtual_pins" "remove_menu_ui_ke" + fi;; + 9) + if [ ! -f "$GIT_BACKUP_FILE" ]; then + error_msg "Git Backup is not installed!" + else + run "remove_git_backup" "remove_menu_ui_ke" + fi;; + 10) + if [ ! -f "$TIMELAPSE_FILE" ]; then + error_msg "Moonraker Timelapse is not installed!" + else + run "remove_moonraker_timelapse" "remove_menu_ui_ke" + fi;; + 11) + if [ ! -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "OctoEverywhere is not installed!" + else + run "remove_octoeverywhere" "remove_menu_ui_ke" + fi;; + 12) + if [ ! -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Moonraker Obico is not installed!" + else + run "remove_moonraker_obico" "remove_menu_ui_ke" + fi;; + 13) + if [ ! -d "$MOBILERAKER_COMPANION_FOLDER" ]; then + error_msg "Mobileraker Companion is not installed!" + else + run "remove_mobileraker_companion" "remove_menu_ui_ke" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + remove_menu_ke +} diff --git a/scripts/menu/KE/tools_menu_KE.sh b/scripts/menu/KE/tools_menu_KE.sh new file mode 100755 index 0000000..eca409b --- /dev/null +++ b/scripts/menu/KE/tools_menu_KE.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +set -e + +function tools_menu_ui_ke() { + top_line + title '[ TOOLS MENU ]' "${yellow}" + inner_line + hr + menu_option ' 1' 'Prevent updating' 'Klipper configuration files' + menu_option ' 2' 'Allow updating' 'Klipper configuration files' + hr + menu_option ' 3' 'Restart' 'Nginx service' + menu_option ' 4' 'Restart' 'Moonraker service' + menu_option ' 5' 'Restart' 'Klipper service' + hr + menu_option ' 6' 'Update' 'Entware packages' + hr + menu_option ' 7' 'Clear' 'cache' + menu_option ' 8' 'Clear' 'logs files' + hr + menu_option ' 9' 'Restore' 'a previous firmware' + hr + menu_option '10' 'Reset' 'factory settings' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function tools_menu_ke() { + clear + tools_menu_ui_ke + local tools_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" tools_menu_opt + case "${tools_menu_opt}" in + 1) + if [ -f "$INITD_FOLDER"/disabled.S55klipper_service ]; then + error_msg "Updating Klipper configuration files is already prevented!" + else + run "prevent_updating_klipper_files" "tools_menu_ui_ke" + fi;; + 2) + if [ ! -f "$INITD_FOLDER"/disabled.S55klipper_service ]; then + error_msg "Updating Klipper configuration files is already allowed!" + else + run "allow_updating_klipper_files" "tools_menu_ui_ke" + fi;; + 3) + if [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Nginx is not installed!" + else + run "restart_nginx_action" "tools_menu_ui_ke" + fi;; + 4) + if [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker is not installed!" + else + run "restart_moonraker_action" "tools_menu_ui_ke" + fi;; + 5) + if [ ! -f "$INITD_FOLDER"/S55klipper_service ]; then + error_msg "Klipper service is not present!" + else + run "restart_klipper_action" "tools_menu_ui_ke" + fi;; + 6) + if [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is not installed!" + else + run "update_entware_packages" "tools_menu_ui_ke" + fi;; + 7) + run "clear_cache" "tools_menu_ui_ke";; + 8) + run "clear_logs" "tools_menu_ui_ke";; + 9) + run "restore_previous_firmware" "tools_menu_ui_ke";; + 10) + run "reset_factory_settings" "tools_menu_ui_ke";; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + tools_menu_ke +} diff --git a/scripts/menu/backup_restore_menu.sh b/scripts/menu/backup_restore_menu.sh new file mode 100755 index 0000000..cee877a --- /dev/null +++ b/scripts/menu/backup_restore_menu.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -e + +function backup_restore_menu_ui() { + top_line + title '[ BACKUP & RESTORE MENU ]' "${yellow}" + inner_line + hr + menu_option '1' 'Backup' 'Klipper configuration files' + menu_option '2' 'Restore' 'Klipper configuration files' + hr + menu_option '3' 'Backup' 'Moonraker database' + menu_option '4' 'Restore' 'Moonraker database' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function backup_restore_menu() { + clear + backup_restore_menu_ui + local backup_restore_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" backup_restore_menu_opt + case "${backup_restore_menu_opt}" in + 1) + run "backup_klipper_config_files" "backup_restore_menu_ui";; + 2) + if [ ! -f "$KLIPPER_CONFIG_FOLDER"/backup_config.tar.gz ]; then + error_msg "Please backup Klipper configuration files before restore!" + else + run "restore_klipper_config_files" "backup_restore_menu_ui" + fi;; + 3) + run "backup_moonraker_database" "backup_restore_menu_ui";; + 4) + if [ ! -f "$KLIPPER_CONFIG_FOLDER"/backup_database.tar.gz ]; then + error_msg "Please backup Moonraker database before restore!" + else + run "restore_moonraker_database" "backup_restore_menu_ui" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + backup_restore_menu +} diff --git a/scripts/menu/customize_menu.sh b/scripts/menu/customize_menu.sh new file mode 100755 index 0000000..caee57c --- /dev/null +++ b/scripts/menu/customize_menu.sh @@ -0,0 +1,106 @@ +#!/bin/sh + +set -e + +function customize_menu_ui() { + top_line + title '[ CUSTOMIZE MENU ]' "${yellow}" + inner_line + hr + menu_option '1' 'Install' 'Custom Boot Display' + menu_option '2' 'Remove' 'Custom Boot Display' + hr + menu_option '3' 'Remove' 'Creality Web Interface' + menu_option '4' 'Restore' 'Creality Web Interface' + hr + menu_option '5' 'Install' 'Guppy Screen' + menu_option '6' 'Remove' 'Guppy Screen' + hr + menu_option '7' 'Install' 'Creality Dynamic Logos for Fluidd' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function customize_menu() { + clear + customize_menu_ui + local customize_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" customize_menu_opt + case "${customize_menu_opt}" in + 1) + if [ -f "$BOOT_DISPLAY_FILE" ]; then + error_msg "Custom Boot Display is already installed!" + elif [ ! -d "$BOOT_DISPLAY_FOLDER" ]; then + error_msg "Please use latest firmware to install Custom Boot Display!" + else + run "install_custom_boot_display" "customize_menu_ui" + fi;; + 2) + if [ ! -f "$BOOT_DISPLAY_FILE" ]; then + error_msg "Custom Boot Display is not installed!" + elif [ ! -d "$BOOT_DISPLAY_FOLDER" ]; then + error_msg "Please use latest firmware to restore Stock Boot Display!" + else + run "remove_custom_boot_display" "customize_menu_ui" + fi;; + 3) + if [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install it first!" + elif [ ! -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is already removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to change the default Web Interface.${white}" + echo + else + run "remove_creality_web_interface" "customize_menu_ui" + fi;; + 4) + if [ -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is already present!" + else + run "restore_creality_web_interface" "customize_menu_ui" + fi;; + 5) + if [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen is already installed!" + echo -e " ${darkred}Please remove Guppy Screen first if you want to change the theme.${white}" + echo + elif [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Please remove Improved Shapers Calibrations first, Guppy Screen already use it!" + elif [ ! -f /lib/ld-2.29.so ]; then + error_msg "Make sure you're running 1.3.x.x firmware version!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_guppy_screen" "customize_menu_ui" + fi;; + 6) + if [ ! -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen is not installed!" + else + run "remove_guppy_screen" "customize_menu_ui" + fi;; + 7) + if [ -f "$FLUIDD_LOGO_FILE" ]; then + error_msg "Creality Dynamic Logos for Fluidd are already installed!" + elif [ ! -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is needed, please install it first!" + else + run "install_creality_dynamic_logos" "customize_menu_ui" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + customize_menu +} diff --git a/scripts/menu/functions.sh b/scripts/menu/functions.sh new file mode 100755 index 0000000..3373e78 --- /dev/null +++ b/scripts/menu/functions.sh @@ -0,0 +1,197 @@ +#!/bin/sh + +set -e + +function top_line() { + echo -e "${white}" + echo -e " ┌──────────────────────────────────────────────────────────────┐" +} + +function hr() { + echo -e " │ │" +} + +function inner_line() { + echo -e " ├──────────────────────────────────────────────────────────────┤" +} + +function bottom_line() { + echo -e " └──────────────────────────────────────────────────────────────┘" + echo -e "${white}" +} + +function blank_line() { + echo -e " " +} + +function title() { + local text=$1 + local color=$2 + local max_length=62 + local text_length=${#text} + local padding_left=$(((max_length - text_length) / 2)) + local padding_right=$((max_length - text_length - padding_left)) + printf " │%*s${color}%s${white}%*s│\n" $padding_left '' "$text" $padding_right '' +} + +function subtitle() { + local menu_text1=$1 + local max_length=61 + local padding=$((max_length - ${#menu_text1})) + printf " │ ${blue}${menu_text1}%-${padding}s${white}│\n" '' +} + +function main_menu_option() { + local menu_number=$1 + local menu_text1=$2 + local menu_text2=$3 + local max_length=56 + local total_text_length=$(( ${#menu_text1} + ${#menu_text2} )) + local padding=$((max_length - total_text_length)) + printf " │ ${yellow}${menu_number}${white}) ${green}${menu_text1} ${white}${menu_text2}%-${padding}s${white}│\n" '' +} + +function menu_option() { + local menu_number=$1 + local menu_text1=$2 + local menu_text2=$3 + local max_length=60 + local total_text_length=$(( ${#menu_text1} + ${#menu_text2} + ${#menu_number} + 4 )) + local padding=$((max_length - total_text_length)) + printf " │ ${yellow}${menu_number}${white}) ${white}${menu_text1} ${green}${menu_text2}%-${padding}s${white}│\n" '' +} + +function bottom_menu_option() { + local menu_number=$1 + local menu_text=$2 + local color=$3 + local max_length=57 + local padding=$((max_length - ${#menu_text})) + printf " │ $color${menu_number}${white}) ${white}${menu_text}%-${padding}s${white}│\n" '' +} + +function info_line() { + local status=$1 + local text=$2 + local color=$3 + local max_length=66 + local total_text_length=$(( ${#status} + ${#text} )) + local padding=$((max_length - total_text_length)) + printf " │ $color${status} ${white}${text}%-${padding}s${white}│\n" '' +} + +function system_line() { + local title="$1" + local value="$2" + local max_length=61 + local title_length=${#title} + local separator=": " + local value_length=${#value} + local value_padding=$((max_length - title_length - ${#separator} - value_length)) + printf " │ ${green}%s${white}%s${white}\e[97m%s%-*s%s${white}│\n" "$title" "$separator" "$value" $value_padding '' +} + +function install_msg() { + read -p "${white} Are you sure you want to install ${green}${1} ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" $2 +} + +function remove_msg() { + read -p "${white} Are you sure you want to remove ${green}${1} ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" $2 +} + +function restore_msg() { + read -p "${white} Are you sure you want to restore ${green}${1} ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" $2 +} + +function backup_msg() { + read -p "${white} Are you sure you want to backup ${green}${1} ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" $2 +} + +function restart_msg() { + read -p "${white} Are you sure you want to restart ${green}${1} ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" $2 +} + +function ok_msg() { + echo + echo -e "${white}${green} ✓ ${1}${white}" + echo +} + +function error_msg() { + echo + echo -e "${white}${darkred} ✗ ${1}${white}" + echo +} + +function run() { + clear + # $1 - Action performed + $1 + # $2 - Menu launched after action is completed + $2 +} + +function check_ipaddress() { + eth0_ip=$(ip -4 addr show eth0 2>/dev/null | grep -o -E '(inet\s)([0-9]+\.){3}[0-9]+' | cut -d ' ' -f 2 | head -n 1) + wlan0_ip=$(ip -4 addr show wlan0 | grep -o -E '(inet\s)([0-9]+\.){3}[0-9]+' | cut -d ' ' -f 2 | head -n 1) + if [ -n "$eth0_ip" ]; then + echo -e "$eth0_ip" + elif [ -n "$wlan0_ip" ]; then + echo -e "$wlan0_ip" + else + echo -e "xxx.xxx.xxx.xxx" + fi +} + +function start_moonraker() { + set +e + /etc/init.d/S56moonraker_service start + sleep 1 + set -e +} + +function stop_moonraker() { + set +e + /etc/init.d/S56moonraker_service stop + sleep 1 + set -e +} + +function start_nginx() { + set +e + /etc/init.d/S50nginx start + sleep 1 + set -e +} + +function stop_nginx() { + set +e + /etc/init.d/S50nginx stop + sleep 1 + set -e +} + +function restart_nginx() { + set +e + /etc/init.d/S50nginx restart + sleep 1 + set -e +} + +function start_klipper() { + set +e + /etc/init.d/S55klipper_service start + set -e +} + +function stop_klipper() { + set +e + /etc/init.d/S55klipper_service stop + set -e +} + +function restart_klipper() { + set +e + /etc/init.d/S55klipper_service restart + set -e +} \ No newline at end of file diff --git a/scripts/menu/info_menu.sh b/scripts/menu/info_menu.sh new file mode 100755 index 0000000..7aa3981 --- /dev/null +++ b/scripts/menu/info_menu.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +set -e + +function check_folder() { + local folder_path="$1" + if [ -d "$folder_path" ]; then + echo -e "${green}✓" + else + echo -e "${red}✗" + fi +} + +function check_file() { + local file_path="$1" + if [ -f "$file_path" ]; then + echo -e "${green}✓" + else + echo -e "${red}✗" + fi +} + +function info_menu_ui() { + top_line + title '[ INFORMATIONS MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + info_line "$(check_folder "$MOONRAKER_FOLDER")" 'Moonraker & Nginx' + info_line "$(check_folder "$FLUIDD_FOLDER")" 'Fluidd' + info_line "$(check_folder "$MAINSAIL_FOLDER")" 'Mainsail' + hr + subtitle '•UTILITIES:' + info_line "$(check_file "$ENTWARE_FILE")" 'Entware' + info_line "$(check_file "$KLIPPER_SHELL_FILE")" 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + info_line "$(check_folder "$KAMP_FOLDER")" 'Klipper Adaptive Meshing & Purging' + info_line "$(check_file "$BUZZER_FILE")" 'Buzzer Support' + info_line "$(check_folder "$NOZZLE_CLEANING_FOLDER")" 'Nozzle Cleaning Fan Control' + info_line "$(check_file "$FAN_CONTROLS_FILE")" 'Fans Control Macros' + info_line "$(check_folder "$IMP_SHAPERS_FOLDER")" 'Improved Shapers Calibrations' + info_line "$(check_file "$USEFUL_MACROS_FILE")" 'Useful Macros' + info_line "$(check_file "$SAVE_ZOFFSET_FILE")" 'Save Z-Offset Macros' + info_line "$(check_file "$SCREWS_ADJUST_FILE")" 'Screws Tilt Adjust Support' + info_line "$(check_file "$VIRTUAL_PINS_FILE")" 'Virtual Pins Support' + info_line "$(check_file "$M600_SUPPORT_FILE")" 'M600 Support' + info_line "$(check_file "$GIT_BACKUP_FILE")" 'Git Backup' + hr + subtitle '•CAMERA:' + info_line "$(check_file "$TIMELAPSE_FILE")" 'Moonraker Timelapse' + info_line "$(check_file "$CAMERA_SETTINGS_FILE")" 'Camera Settings Control' + hr + subtitle '•REMOTE ACCESS AND AI DETECTION:' + info_line "$(check_folder "$OCTOEVERYWHERE_FOLDER")" 'OctoEverywhere' + info_line "$(check_folder "$MOONRAKER_OBICO_FOLDER")" 'Obico' + info_line "$(check_folder "$MOBILERAKER_COMPANION_FOLDER")" 'Mobileraker Companion' + hr + subtitle '•CUSTOMIZATION:' + info_line "$(check_file "$BOOT_DISPLAY_FILE")" 'Custom Boot Display' + info_line "$(check_file "$CREALITY_WEB_FILE")" 'Creality Web Interface' + info_line "$(check_folder "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' + info_line "$(check_file "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function info_menu() { + clear + info_menu_ui + local info_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" info_menu_opt + case "${info_menu_opt}" in + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + info_menu +} diff --git a/scripts/menu/install_menu.sh b/scripts/menu/install_menu.sh new file mode 100755 index 0000000..5e16e00 --- /dev/null +++ b/scripts/menu/install_menu.sh @@ -0,0 +1,228 @@ +#!/bin/sh + +set -e + +function install_menu_ui() { + top_line + title '[ INSTALL MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + menu_option ' 1' 'Install' 'Moonraker and Nginx' + menu_option ' 2' 'Install' 'Fluidd (port 4408)' + menu_option ' 3' 'Install' 'Mainsail (port 4409)' + hr + subtitle '•UTILITIES:' + menu_option ' 4' 'Install' 'Entware' + menu_option ' 5' 'Install' 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + menu_option ' 6' 'Install' 'Klipper Adaptive Meshing & Purging' + menu_option ' 7' 'Install' 'Buzzer Support' + menu_option ' 8' 'Install' 'Nozzle Cleaning Fan Control' + menu_option ' 9' 'Install' 'Fans Control Macros' + menu_option '10' 'Install' 'Improved Shapers Calibrations' + menu_option '11' 'Install' 'Useful Macros' + menu_option '12' 'Install' 'Save Z-Offset Macros' + menu_option '13' 'Install' 'Screws Tilt Adjust Support' + menu_option '14' 'Install' 'Virtual Pins Support' + menu_option '15' 'Install' 'M600 Support' + menu_option '16' 'Install' 'Git Backup' + hr + subtitle '•CAMERA:' + menu_option '17' 'Install' 'Moonraker Timelapse' + menu_option '18' 'Install' 'Camera Settings Control' + hr + subtitle '•REMOTE ACCESS AND AI DETECTION:' + menu_option '19' 'Install' 'OctoEverywhere' + menu_option '20' 'Install' 'Moonraker Obico' + menu_option '21' 'Install' 'Mobileraker Companion' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function install_menu() { + clear + install_menu_ui + local install_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" install_menu_opt + case "${install_menu_opt}" in + 1) + if [ -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are already installed!" + else + run "install_moonraker_nginx" "install_menu_ui" + fi;; + 2) + if [ -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + else + run "install_fluidd" "install_menu_ui" + fi;; + 3) + if [ -d "$MAINSAIL_FOLDER" ]; then + error_msg "Mainsail is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + else + run "install_mainsail" "install_menu_ui" + fi;; + 4) + if [ -f "$ENTWARE_FILE" ]; then + error_msg "Entware is already installed!" + else + run "install_entware" "install_menu_ui" + fi;; + 5) + if [ -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is already installed!" + else + run "install_gcode_shell_command" "install_menu_ui" + fi;; + 6) + if [ -d "$KAMP_FOLDER" ]; then + error_msg "Klipper Adaptive Meshing & Purging is already installed!" + elif [ ! -f "$VIRTUAL_PINS_FILE" ]; then + error_msg "Virtual Pins Support is needed, please install it first!" + else + run "install_kamp" "install_menu_ui" + fi;; + 7) + if [ -f "$BUZZER_FILE" ]; then + error_msg "Buzzer Support is already installed!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_buzzer_support" "install_menu_ui" + fi;; + 8) + if [ -d "$NOZZLE_CLEANING_FOLDER" ]; then + error_msg "Nozzle Cleaning Fan Control is already installed!" + else + run "install_nozzle_cleaning_fan_control" "install_menu_ui" + fi;; + 9) + if [ -f "$FAN_CONTROLS_FILE" ]; then + error_msg "Fans Control Macros are already installed!" + else + run "install_fans_control_macros" "install_menu_ui" + fi;; + 10) + if [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Improved Shapers Calibrations are already installed!" + elif [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Guppy Screen already has these features!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_improved_shapers" "install_menu_ui" + fi;; + 11) + if [ -f "$USEFUL_MACROS_FILE" ]; then + error_msg "Useful Macros are already installed!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_useful_macros" "install_menu_ui" + fi;; + 12) + if [ -f "$SAVE_ZOFFSET_FILE" ]; then + error_msg "Save Z-Offset Macros are already installed!" + else + run "install_save_zoffset_macros" "install_menu_ui" + fi;; + 13) + if [ -f "$SCREWS_ADJUST_FILE" ]; then + error_msg "Screws Tilt Adjust Support is already installed!" + else + run "install_screws_tilt_adjust" "install_menu_ui" + fi;; + 14) + if [ -f "$VIRTUAL_PINS_FILE" ]; then + error_msg "Virtual Pins Support is already installed!" + else + run "install_virtual_pins" "install_menu_ui" + fi;; + 15) + if [ -f "$M600_SUPPORT_FILE" ]; then + error_msg "M600 Support is already installed!" + else + run "install_m600_support" "install_menu_ui" + fi;; + 16) + if [ -f "$GIT_BACKUP_FILE" ]; then + error_msg "Git Backup is already installed!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_git_backup" "install_menu_ui" + fi;; + 17) + if [ -f "$TIMELAPSE_FILE" ]; then + error_msg "Moonraker Timelapse is already installed!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_moonraker_timelapse" "install_menu_ui" + fi;; + 18) + if [ -f "$CAMERA_SETTINGS_FILE" ]; then + error_msg "Camera Settings Control is already installed!" + elif v4l2-ctl --list-devices | grep -q 'CCX2F3299'; then + error_msg "You have the new hardware version of the camera and it's not compatible!" + elif [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed, please install it first!" + else + run "install_camera_settings_control" "install_menu_ui" + fi;; + 19) + if [ -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "OctoEverywhere is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install it first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_octoeverywhere" "install_menu_ui" + fi;; + 20) + if [ -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Moonraker Obico is already installed!" + elif [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker and Nginx are needed, please install them first!" + elif [ ! -d "$FLUIDD_FOLDER" ] && [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Fluidd or Mainsail is needed, please install it first!" + elif [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is needed, please install it first!" + else + run "install_moonraker_obico" "install_menu_ui" + fi;; + 21) + if [ -d "$MOBILERAKER_COMPANION_FOLDER" ]; then + error_msg "Mobileraker Companion is already installed!" + else + run "install_mobileraker_companion" "install_menu_ui" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + install_menu +} diff --git a/scripts/menu/main_menu.sh b/scripts/menu/main_menu.sh new file mode 100755 index 0000000..15697ea --- /dev/null +++ b/scripts/menu/main_menu.sh @@ -0,0 +1,121 @@ +#!/bin/sh + +set -e + +if /usr/bin/get_sn_mac.sh model 2>&1 | grep -iq "K1"; then K1=1; else K1=0; fi + +function get_script_version() { + local version + cd "${HELPER_SCRIPT_FOLDER}" + version="$(git describe HEAD --always --tags | sed 's/-.*//')" + echo "${cyan}${version}${white}" +} + +function version_line() { + local content="$1" + local content_length="${#content}" + local width=$((73)) + local padding_length=$((width - content_length - 3)) + printf " │ %*s%s%s\n" $padding_length '' "$content" " │" +} + +function script_title() { + local title + if [ $K1 -eq 0 ]; then + title="KE" + else + title="K1" + fi + echo "${title}" +} + +function fw_version() { + local firmware + if [ $K1 -eq 0 ]; then + firmware="1.1.0.12" + else + firmware="1.3.3.5" + fi + echo "${firmware}" +} + +function main_menu_ui() { + top_line + title "• HELPER SCRIPT FOR CREALITY $(script_title) SERIES •" "${blue}" + title "Copyright © Cyril Guislain (Guilouz)" "${white}" + inner_line + title "/!\\ ONLY USE IT WITH FIRMWARE $(fw_version) AND ABOVE /!\\" "${darkred}" + inner_line + hr + main_menu_option '1' '[Install]' 'Menu' + main_menu_option '2' '[Remove]' 'Menu' + main_menu_option '3' '[Customize]' 'Menu' + main_menu_option '4' '[Backup & Restore]' 'Menu' + main_menu_option '5' '[Tools]' 'Menu' + main_menu_option '6' '[Informations]' 'Menu' + main_menu_option '7' '[System]' 'Menu' + hr + inner_line + hr + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function main_menu() { + clear + main_menu_ui + local main_menu_opt + while true; do + read -p "${white} Type your choice and validate with Enter: ${yellow}" main_menu_opt + case "${main_menu_opt}" in + 1) clear + if [ $K1 -eq 0 ]; then + install_menu_ke + else + install_menu + fi + break;; + 2) clear + if [ $K1 -eq 0 ]; then + remove_menu_ke + else + remove_menu + fi + break;; + 3) clear + if [ $K1 -eq 0 ]; then + customize_menu_ke + else + customize_menu + fi + break;; + 4) clear + backup_restore_menu + break;; + 5) clear + if [ $K1 -eq 0 ]; then + tools_menu_ke + else + tools_menu + fi + main_ui;; + 6) clear + if [ $K1 -eq 0 ]; then + info_menu_ke + else + info_menu + fi + break;; + 7) clear + system_menu + break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + main_menu +} diff --git a/scripts/menu/remove_menu.sh b/scripts/menu/remove_menu.sh new file mode 100755 index 0000000..418b8a4 --- /dev/null +++ b/scripts/menu/remove_menu.sh @@ -0,0 +1,220 @@ +#!/bin/sh + +set -e + +function remove_menu_ui() { + top_line + title '[ REMOVE MENU ]' "${yellow}" + inner_line + hr + subtitle '•ESSENTIALS:' + menu_option ' 1' 'Remove' 'Moonraker and Nginx' + menu_option ' 2' 'Remove' 'Fluidd (port 4408)' + menu_option ' 3' 'Remove' 'Mainsail (port 4409)' + hr + subtitle '•UTILITIES:' + menu_option ' 4' 'Remove' 'Entware' + menu_option ' 5' 'Remove' 'Klipper Gcode Shell Command' + hr + subtitle '•IMPROVEMENTS:' + menu_option ' 6' 'Remove' 'Klipper Adaptive Meshing & Purging' + menu_option ' 7' 'Remove' 'Buzzer Support' + menu_option ' 8' 'Remove' 'Nozzle Cleaning Fan Control' + menu_option ' 9' 'Remove' 'Fans Control Macros' + menu_option '10' 'Remove' 'Improved Shapers Calibrations' + menu_option '11' 'Remove' 'Useful Macros' + menu_option '12' 'Remove' 'Save Z-Offset Macros' + menu_option '13' 'Remove' 'Screws Tilt Adjust Support' + menu_option '14' 'Remove' 'Virtual Pins Support' + menu_option '15' 'Remove' 'M600 Support' + menu_option '16' 'Remove' 'Git Backup' + hr + subtitle '•CAMERA:' + menu_option '17' 'Remove' 'Moonraker Timelapse' + menu_option '18' 'Remove' 'Camera Settings Control' + hr + subtitle '•REMOTE ACCESS AND AI DETECTION:' + menu_option '19' 'Remove' 'OctoEverywhere' + menu_option '20' 'Remove' 'Moonraker Obico' + menu_option '21' 'Remove' 'Mobileraker Companion' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function remove_menu() { + clear + remove_menu_ui + local remove_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" remove_menu_opt + case "${remove_menu_opt}" in + 1) + if [ ! -d "$MOONRAKER_FOLDER" ] && [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Moonraker and Nginx are not installed!" + else + run "remove_moonraker_nginx" "remove_menu_ui" + fi;; + 2) + if [ ! -d "$FLUIDD_FOLDER" ]; then + error_msg "Fluidd is not installed!" + elif [ ! -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to remove Fluidd.${white}" + echo + else + run "remove_fluidd" "remove_menu_ui" + fi;; + 3) + if [ ! -d "$MAINSAIL_FOLDER" ]; then + error_msg "Mainsail is not installed!" + elif [ ! -f "$CREALITY_WEB_FILE" ]; then + error_msg "Creality Web Interface is removed!" + echo -e " ${darkred}Please restore Creality Web Interface first if you want to remove Mainsail.${white}" + echo + else + run "remove_mainsail" "remove_menu_ui" + fi;; + 4) + if [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is not installed!" + elif [ -f "$TIMELAPSE_FILE" ]; then + error_msg "Entware is needed to use Moonraker Timelapse, please uninstall it first!" + elif [ -f "$GIT_BACKUP_FILE" ]; then + error_msg "Entware is needed to use Git Backup, please uninstall it first!" + elif [ -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "Entware is needed to use OctoEverywhere, please uninstall it first!" + elif [ -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Entware is needed to use Moonraker Obico, please uninstall it first!" + else + run "remove_entware" "remove_menu_ui" + fi;; + 5) + if [ ! -f "$KLIPPER_SHELL_FILE" ]; then + error_msg "Klipper Gcode Shell Command is not installed!" + elif [ -f "$BUZZER_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Buzzer Support, please uninstall it first!" + elif [ -f "$CAMERA_SETTINGS_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Camera Settings Control, please uninstall it first!" + elif [ -d "$GUPPY_SCREEN_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Guppy Screen, please uninstall it first!" + elif [ -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Improved Shapers Calibrations, please uninstall it first!" + elif [ -d "$GIT_BACKUP_FOLDER" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Git Backup, please uninstall it first!" + elif [ -f "$USEFUL_MACROS_FILE" ]; then + error_msg "Klipper Gcode Shell Command is needed to use Useful Macros, please uninstall it first!" + else + run "remove_gcode_shell_command" "remove_menu_ui" + fi;; + 6) + if [ ! -d "$KAMP_FOLDER" ]; then + error_msg "Klipper Adaptive Meshing & Purging is not installed!" + else + run "remove_kamp" "remove_menu_ui" + fi;; + 7) + if [ ! -f "$BUZZER_FILE" ]; then + error_msg "Buzzer Support is not installed!" + else + run "remove_buzzer_support" "remove_menu_ui" + fi;; + 8) + if [ ! -d "$NOZZLE_CLEANING_FOLDER" ]; then + error_msg "Nozzle Cleaning Fan Control is not installed!" + else + run "remove_nozzle_cleaning_fan_control" "remove_menu_ui" + fi;; + 9) + if [ ! -f "$FAN_CONTROLS_FILE" ]; then + error_msg "Fans Control Macros are not installed!" + else + run "remove_fans_control_macros" "remove_menu_ui" + fi;; + 10) + if [ ! -d "$IMP_SHAPERS_FOLDER" ]; then + error_msg "Improved Shapers Calibrations are not installed!" + else + run "remove_improved_shapers" "remove_menu_ui" + fi;; + 11) + if [ ! -f "$USEFUL_MACROS_FILE" ]; then + error_msg "Useful Macros are not installed!" + else + run "remove_useful_macros" "remove_menu_ui" + fi;; + 12) + if [ ! -f "$SAVE_ZOFFSET_FILE" ]; then + error_msg "Save Z-Offset Macros are not installed!" + else + run "remove_save_zoffset_macros" "remove_menu_ui" + fi;; + 13) + if [ ! -f "$SCREWS_ADJUST_FILE" ]; then + error_msg "Screws Tilt Adjust Support is not installed!" + else + run "remove_screws_tilt_adjust" "remove_menu_ui" + fi;; + 14) + if [ ! -f "$VIRTUAL_PINS_FILE" ]; then + error_msg "Virtual Pins Support is not installed!" + else + run "remove_virtual_pins" "remove_menu_ui" + fi;; + 15) + if [ ! -f "$M600_SUPPORT_FILE" ]; then + error_msg "M600 Support is not installed!" + else + run "remove_m600_support" "remove_menu_ui" + fi;; + 16) + if [ ! -f "$GIT_BACKUP_FILE" ]; then + error_msg "Git Backup is not installed!" + else + run "remove_git_backup" "remove_menu_ui" + fi;; + 17) + if [ ! -f "$TIMELAPSE_FILE" ]; then + error_msg "Moonraker Timelapse is not installed!" + else + run "remove_moonraker_timelapse" "remove_menu_ui" + fi;; + 18) + if [ ! -f "$CAMERA_SETTINGS_FILE" ]; then + error_msg "Camera Settings Control is not installed!" + else + run "remove_camera_settings_control" "remove_menu_ui" + fi;; + 19) + if [ ! -d "$OCTOEVERYWHERE_FOLDER" ]; then + error_msg "OctoEverywhere is not installed!" + else + run "remove_octoeverywhere" "remove_menu_ui" + fi;; + 20) + if [ ! -d "$MOONRAKER_OBICO_FOLDER" ]; then + error_msg "Moonraker Obico is not installed!" + else + run "remove_moonraker_obico" "remove_menu_ui" + fi;; + 21) + if [ ! -d "$MOBILERAKER_COMPANION_FOLDER" ]; then + error_msg "Mobileraker Companion is not installed!" + else + run "remove_mobileraker_companion" "remove_menu_ui" + fi;; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + remove_menu +} diff --git a/scripts/menu/system_menu.sh b/scripts/menu/system_menu.sh new file mode 100755 index 0000000..11a763c --- /dev/null +++ b/scripts/menu/system_menu.sh @@ -0,0 +1,80 @@ +#!/bin/sh + +set -e + +function check_fw_version() { + file="/usr/data/creality/userdata/config/system_version.json" + if [ -e "$file" ]; then + cat "$file" | jq -r '.sys_version' + else + echo -e "N/A" + fi +} + +function check_connection() { + eth0_ip=$(ip -4 addr show eth0 2>/dev/null | grep -o -E '(inet\s)([0-9]+\.){3}[0-9]+' | cut -d ' ' -f 2 | head -n 1) + wlan0_ip=$(ip -4 addr show wlan0 | grep -o -E '(inet\s)([0-9]+\.){3}[0-9]+' | cut -d ' ' -f 2 | head -n 1) + if [ -n "$eth0_ip" ]; then + echo -e "$eth0_ip (ETHERNET)" + elif [ -n "$wlan0_ip" ]; then + echo -e "$wlan0_ip (WLAN)" + else + echo -e "xxx.xxx.xxx.xxx" + fi +} + +function system_menu_ui() { + memfree=`cat /proc/meminfo | grep MemFree | awk {'print $2'}` + memtotal=`cat /proc/meminfo | grep MemTotal | awk {'print $2'}` + pourcent=$((($memfree * 100)/$memtotal)) + diskused=`df -h | grep /dev/mmcblk0p10 | awk {'print $3 " / " $2 " (" $4 " available)" '}` + process=`ps ax | wc -l | tr -d " "` + uptime=`cat /proc/uptime | cut -f1 -d.` + upDays=$((uptime/60/60/24)) + upHours=$((uptime/60/60%24)) + upMins=$((uptime/60%60)) + load=`cat /proc/loadavg | awk {'print $1 " (1 min.) / " $2 " (5 min.) / " $3 " (15 min.)"'}` + device_sn=$(cat /usr/data/creality/userdata/config/system_config.json | grep -o '"device_sn":"[^"]*' | awk -F '"' '{print $4}') + mac_address=$(cat /usr/data/creality/userdata/config/system_config.json | grep -o '"device_mac":"[^"]*' | awk -F '"' '{print $4}' | sed 's/../&:/g; s/:$//') + top_line + title '[ SYSTEM MENU ]' "${yellow}" + inner_line + hr + system_line " System" "$(uname -s) (Kernel $(uname -r))" "${green}" + system_line " Firmware" "$(check_fw_version)" + system_line " Hostname" "$(uname -n)" + system_line " Device SN" "$device_sn" + system_line " IP Address" "$(check_connection)" + system_line "MAC Address" "$mac_address" + system_line " RAM Usage" "$(($memfree/1024)) MB / $(($memtotal/1024)) MB ($pourcent% available)" + system_line " Disk Usage" "$diskused" + system_line " Uptime" "$upDays days $upHours hours $upMins minutes" + system_line " Processes" "$process running process" + system_line "System Load" "$load" + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function system_menu() { + clear + system_menu_ui + local system_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" system_menu_opt + case "${system_menu_opt}" in + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + system_menu +} diff --git a/scripts/menu/tools_menu.sh b/scripts/menu/tools_menu.sh new file mode 100755 index 0000000..ee5a74d --- /dev/null +++ b/scripts/menu/tools_menu.sh @@ -0,0 +1,100 @@ +#!/bin/sh + +set -e + +function tools_menu_ui() { + top_line + title '[ TOOLS MENU ]' "${yellow}" + inner_line + hr + menu_option ' 1' 'Prevent updating' 'Klipper configuration files' + menu_option ' 2' 'Allow updating' 'Klipper configuration files' + menu_option ' 3' 'Fix' 'printing Gcode files from folder' + hr + menu_option ' 4' 'Restart' 'Nginx service' + menu_option ' 5' 'Restart' 'Moonraker service' + menu_option ' 6' 'Restart' 'Klipper service' + hr + menu_option ' 7' 'Update' 'Entware packages' + hr + menu_option ' 8' 'Clear' 'cache' + menu_option ' 9' 'Clear' 'logs files' + hr + menu_option '10' 'Restore' 'a previous firmware' + hr + menu_option '11' 'Reset' 'factory settings' + hr + inner_line + hr + bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" + bottom_menu_option 'q' 'Exit' "${darkred}" + hr + version_line "$(get_script_version)" + bottom_line +} + +function tools_menu() { + clear + tools_menu_ui + local tools_menu_opt + while true; do + read -p " ${white}Type your choice and validate with Enter: ${yellow}" tools_menu_opt + case "${tools_menu_opt}" in + 1) + if [ -f "$INITD_FOLDER"/disabled.S55klipper_service ]; then + error_msg "Updating Klipper configuration files is already prevented!" + else + run "prevent_updating_klipper_files" "tools_menu_ui" + fi;; + 2) + if [ ! -f "$INITD_FOLDER"/disabled.S55klipper_service ]; then + error_msg "Updating Klipper configuration files is already allowed!" + else + run "allow_updating_klipper_files" "tools_menu_ui" + fi;; + 3) + if [ -f "$KLIPPER_KLIPPY_FOLDER"/gcode.py ]; then + run "printing_gcode_from_folder" "tools_menu_ui" + fi;; + 4) + if [ ! -d "$NGINX_FOLDER" ]; then + error_msg "Nginx is not installed!" + else + run "restart_nginx_action" "tools_menu_ui" + fi;; + 5) + if [ ! -d "$MOONRAKER_FOLDER" ]; then + error_msg "Moonraker is not installed!" + else + run "restart_moonraker_action" "tools_menu_ui" + fi;; + 6) + if [ ! -f "$INITD_FOLDER"/S55klipper_service ]; then + error_msg "Klipper service is not present!" + else + run "restart_klipper_action" "tools_menu_ui" + fi;; + 7) + if [ ! -f "$ENTWARE_FILE" ]; then + error_msg "Entware is not installed!" + else + run "update_entware_packages" "tools_menu_ui" + fi;; + 8) + run "clear_cache" "tools_menu_ui";; + 9) + run "clear_logs" "tools_menu_ui";; + 10) + run "restore_previous_firmware" "tools_menu_ui";; + 11) + run "reset_factory_settings" "tools_menu_ui";; + B|b) + clear; main_menu; break;; + Q|q) + clear; exit 0;; + *) + error_msg "Please select a correct choice!";; + esac + done + tools_menu +} diff --git a/scripts/mobileraker_companion.sh b/scripts/mobileraker_companion.sh new file mode 100755 index 0000000..bb2fbc7 --- /dev/null +++ b/scripts/mobileraker_companion.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +set -e + +function mobileraker_companion_message(){ + top_line + title 'Mobileraker Companion' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Mobileraker Companion allows to push notification for ${white}│" + echo -e " │ ${cyan}Klipper using Moonraker for Mobileraker phone App. ${white}│" + hr + bottom_line +} + +function install_mobileraker_companion(){ + mobileraker_companion_message + local yn + while true; do + install_msg "Mobileraker Companion" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Downloading Mobileraker Companion..." + git config --global http.sslVerify false + git clone "$MOBILERAKER_COMPANION_URL" "$MOBILERAKER_COMPANION_FOLDER" + echo -e "Info: Running Mobileraker Companion installer..." + sh "$MOBILERAKER_COMPANION_FOLDER"/scripts/install.sh + echo + if grep -q "#\[update_manager mobileraker\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Enabling Mobileraker Companion configurations for Update Manager..." + sed -i -e 's/^\s*#[[:space:]]*\[update_manager mobileraker\]/[update_manager mobileraker]/' -e '/^\[update_manager mobileraker\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MOONRAKER_CFG" + else + echo -e "Info: Mobileraker Companion configurations are already enabled for Update Manager..." + fi + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Mobileraker Companion has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_mobileraker_companion(){ + mobileraker_companion_message + local yn + while true; do + remove_msg "Mobileraker Companion" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Running Mobileraker Companion uninstaller..." + sh "$MOBILERAKER_COMPANION_FOLDER"/scripts/install.sh -uninstall + echo + if grep -q "\[update_manager mobileraker\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Disabling Mobileraker Companion configurations for Update Manager..." + sed -i '/^\[update_manager mobileraker\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MOONRAKER_CFG" + else + echo -e "Info: Mobileraker Companion configurations are already disabled for Update Manager..." + fi + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Mobileraker Companion has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/moonraker_nginx.sh b/scripts/moonraker_nginx.sh new file mode 100755 index 0000000..92c1c70 --- /dev/null +++ b/scripts/moonraker_nginx.sh @@ -0,0 +1,107 @@ +#!/bin/sh + +set -e + +function moonraker_nginx_message(){ + top_line + title 'Moonraker and Nginx' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Moonraker is a Python 3 based web server that exposes APIs ${white}│" + echo -e " │ ${cyan}with which client applications may use to interact with ${white}│" + echo -e " │ ${cyan}Klipper firmware. ${white}│" + echo -e " │ ${cyan}Nginx is a web server that can also be used as a reverse ${white}│" + echo -e " │ ${cyan}proxy, load balancer, mail proxy and HTTP cache. ${white}│" + hr + bottom_line +} + +function install_moonraker_nginx(){ + moonraker_nginx_message + local yn + while true; do + install_msg "Moonraker and Nginx" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Extracting files..." + tar -xvf "$MOONRAKER_URL1" -C "$USR_DATA" + echo -e "Info: Copying services files..." + if [ ! -f "$INITD_FOLDER"/S50nginx ]; then + cp "$NGINX_SERVICE_URL" "$INITD_FOLDER"/S50nginx + chmod +x "$INITD_FOLDER"/S50nginx + fi + if [ ! -f "$INITD_FOLDER"/S56moonraker_service ]; then + cp "$MOONRAKER_SERVICE_URL" "$INITD_FOLDER"/S56moonraker_service + chmod +x "$INITD_FOLDER"/S56moonraker_service + fi + echo -e "Info: Copying Moonraker configuration file..." + cp "$MOONRAKER_URL2" "$KLIPPER_CONFIG_FOLDER"/moonraker.conf + if [ -f "$PRINTER_DATA_FOLDER"/moonraker.asvc ]; then + rm -f "$PRINTER_DATA_FOLDER"/moonraker.asvc + fi + cp "$MOONRAKER_URL3" "$PRINTER_DATA_FOLDER"/moonraker.asvc + echo -e "Info: Applying changes from official repo..." + cd "$MOONRAKER_FOLDER"/moonraker + git stash; git checkout master; git pull + echo -e "Info: Installing Supervisor Lite..." + chmod 755 "$SUPERVISOR_URL" + ln -sf "$SUPERVISOR_URL" "$SUPERVISOR_FILE" + echo -e "Info: Installing Host Controls Support..." + chmod 755 "$SUDO_URL" + chmod 755 "$SYSTEMCTL_URL" + ln -sf "$SUDO_URL" "$SUDO_FILE" + ln -sf "$SYSTEMCTL_URL" "$SYSTEMCTL_FILE" + echo -e "Info: Installing necessary packages..." + cd "$MOONRAKER_FOLDER"/moonraker-env/bin + python3 -m pip install --no-cache-dir pyserial-asyncio==0.6 + echo -e "Info: Starting Nginx service..." + start_nginx + echo -e "Info: Starting Moonraker service..." + start_moonraker + ok_msg "Moonraker and Nginx have been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_moonraker_nginx(){ + moonraker_nginx_message + local yn + while true; do + remove_msg "Moonraker and Nginx" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Stopping Moonraker and Nginx services..." + cd /overlay/upper + stop_moonraker + stop_nginx + echo -e "Info: Removing files..." + rm -f "$INITD_FOLDER"/S50nginx + rm -f "$INITD_FOLDER"/S56moonraker_service + rm -f "$KLIPPER_CONFIG_FOLDER"/moonraker.conf + rm -f "$KLIPPER_CONFIG_FOLDER"/.moonraker.conf.bkp + rm -f "$PRINTER_DATA_FOLDER"/.moonraker.uuid + rm -f "$PRINTER_DATA_FOLDER"/moonraker.asvc + rm -rf "$PRINTER_DATA_FOLDER"/comms + rm -rf "$NGINX_FOLDER" + rm -rf "$MOONRAKER_FOLDER" + rm -f "$SUPERVISOR_FILE" + rm -f "$SUDO_FILE" + rm -f "$SYSTEMCTL_FILE" + ok_msg "Moonraker and Nginx have been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/moonraker_obico.sh b/scripts/moonraker_obico.sh new file mode 100755 index 0000000..da8db6a --- /dev/null +++ b/scripts/moonraker_obico.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +set -e + +function moonraker_obico_message(){ + top_line + title 'Moonraker Obico' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Obico is a Moonraker plugin that allows you to monitor and ${white}│" + echo -e " │ ${cyan}control your 3D printer from anywhere. ${white}│" + hr + bottom_line +} + +function install_moonraker_obico(){ + moonraker_obico_message + local yn + while true; do + install_msg "Moonraker Obico" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -d "$MOONRAKER_OBICO_FOLDER" ]; then + echo -e "Info: Moonraker Obico is already installed. Download skipped." + else + echo -e "Info: Downloading Moonraker Obico..." + git config --global http.sslVerify false + git clone "$MOONRAKER_OBICO_URL" "$MOONRAKER_OBICO_FOLDER" + fi + echo -e "Info: Running Moonraker Obico installer..." + cd "$MOONRAKER_OBICO_FOLDER" + sh ./scripts/install_creality.sh -k + ok_msg "Moonraker Obico has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_moonraker_obico(){ + moonraker_obico_message + local yn + while true; do + remove_msg "Moonraker Obico" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if grep -q "include moonraker_obico_macros" "$PRINTER_CFG" ; then + echo -e "Info: Removing Moonraker Obico configurations in printer.cfg file..." + sed -i '/include moonraker_obico_macros\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Moonraker Obico configurations are already removed in printer.cfg file..." + fi + if grep -q "\[include moonraker-obico-update.cfg\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Removing Moonraker Obico configurations in moonraker.conf file..." + sed -i '/include moonraker-obico-update\.cfg/d' "$MOONRAKER_CFG" + else + echo -e "Info: Moonraker Obico configurations are already removed in moonraker.conf file..." + fi + echo -e "Info: Removing files..." + rm -rf "$MOONRAKER_OBICO_FOLDER" + rm -rf /usr/data/moonraker-obico-env + rm -f "$KLIPPER_CONFIG_FOLDER"/moonraker-obico-update.cfg + rm -f "$KLIPPER_CONFIG_FOLDER"/config/moonraker-obico.cfg + rm -f /etc/init.d/S99moonraker_obico + if [ -f "$ENTWARE_FILE" ]; then + echo -e "Info: Removing packages..." + "$ENTWARE_FILE" --autoremove remove python3 + "$ENTWARE_FILE" --autoremove remove python3-pip + fi + echo -e "Info: Restarting Moonraker service..." + stop_moonraker + start_moonraker + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Moonraker Obico has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/moonraker_timelapse.sh b/scripts/moonraker_timelapse.sh new file mode 100755 index 0000000..92c1afb --- /dev/null +++ b/scripts/moonraker_timelapse.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +set -e + +function moonraker_timelapse_message(){ + top_line + title 'Moonraker Timelapse' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Moonraker Timelapse is a 3rd party Moonraker component to ${white}│" + echo -e " │ ${cyan}create timelapse of 3D prints. ${white}│" + hr + bottom_line +} + +function install_moonraker_timelapse(){ + moonraker_timelapse_message + local yn + while true; do + install_msg "Moonraker Timelapse" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/timelapse.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/timelapse.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Linking file..." + ln -sf "$TIMELAPSE_URL1" "$TIMELAPSE_FILE" + ln -sf "$TIMELAPSE_URL2" "$HS_CONFIG_FOLDER"/timelapse.cfg + if grep -q "include Helper-Script/timelapse" "$PRINTER_CFG" ; then + echo -e "Info: Moonraker Timelapse configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Moonraker Timelapse configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/timelapse\.cfg\]' "$PRINTER_CFG" + fi + if grep -q "#\[timelapse\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Enabling Moonraker Timelapse configurations in moonraker.conf file..." + sed -i -e 's/^\s*#[[:space:]]*\[timelapse\]/[timelapse]/' -e '/^\[timelapse\]/,/^\s*$/ s/^\(\s*\)#/\1/' "$MOONRAKER_CFG" + else + echo -e "Info: Moonraker Timelapse configurations are already enabled in moonraker.conf file..." + fi + echo -e "Info: Updating ffmpeg..." + "$ENTWARE_FILE" update && "$ENTWARE_FILE" upgrade ffmpeg + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Moonraker Timelapse has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_moonraker_timelapse(){ + moonraker_timelapse_message + local yn + while true; do + remove_msg "Moonraker Timelapse" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing files..." + rm -f "$HS_CONFIG_FOLDER"/timelapse.cfg + rm -f /usr/data/moonraker/moonraker/moonraker/components/timelapse.py + rm -f /usr/data/moonraker/moonraker/moonraker/components/timelapse.pyc + if [ -f /opt/bin/ffmpeg ]; then + "$ENTWARE_FILE" --autoremove remove ffmpeg + fi + if grep -q "include Helper-Script/timelapse" "$PRINTER_CFG" ; then + echo -e "Info: Removing Moonraker Timelapse configurations in printer.cfg file..." + sed -i '/include Helper-Script\/timelapse\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Moonraker Timelapse configurations are already removed in printer.cfg file..." + fi + if grep -q "\[timelapse\]" "$MOONRAKER_CFG" ; then + echo -e "Info: Disabling Moonraker Timelapse configurations in moonraker.conf file..." + sed -i '/^\[timelapse\]/,/^\s*$/ s/^\(\s*\)\([^#]\)/#\1\2/' "$MOONRAKER_CFG" + else + echo -e "Info: Moonraker Timelapse configurations are already disabled in moonraker.conf file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Moonraker Timelapse has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/nozzle_cleaning_fan_control.sh b/scripts/nozzle_cleaning_fan_control.sh new file mode 100755 index 0000000..3d2fee1 --- /dev/null +++ b/scripts/nozzle_cleaning_fan_control.sh @@ -0,0 +1,85 @@ +#!/bin/sh + +set -e + +function nozzle_cleaning_fan_control_message(){ + top_line + title 'Nozzle Cleaning Fan Control' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This is an Klipper extension to control fans during nozzle ${white}│" + echo -e " │ ${cyan}cleaning. ${white}│" + hr + bottom_line +} + +function install_nozzle_cleaning_fan_control(){ + nozzle_cleaning_fan_control_message + local yn + while true; do + install_msg "Nozzle Cleaning Fan Control" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -d "NOZZLE_CLEANING_FOLDER" ]; then + rm -rf "NOZZLE_CLEANING_FOLDER" + fi + mkdir -p "$NOZZLE_CLEANING_FOLDER" + echo -e "Info: Linking files..." + ln -sf "$NOZZLE_CLEANING_URL1" "$NOZZLE_CLEANING_FOLDER"/__init__.py + ln -sf "$NOZZLE_CLEANING_URL2" "$NOZZLE_CLEANING_FOLDER"/prtouch_v2_fan.pyc + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + ln -sf "$NOZZLE_CLEANING_URL3" "$HS_CONFIG_FOLDER"/nozzle-cleaning-fan-control.cfg + if grep -q "include Helper-Script/nozzle-cleaning-fan-control" "$PRINTER_CFG" ; then + echo -e "Info: Nozzle Cleaning Fan Control configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Nozzle Cleaning Fan Control configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/nozzle-cleaning-fan-control\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Nozzle Cleaning Fan Control has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_nozzle_cleaning_fan_control(){ + nozzle_cleaning_fan_control_message + local yn + while true; do + remove_msg "Nozzle Cleaning Fan Control" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing files..." + rm -rf "$NOZZLE_CLEANING_FOLDER" + rm -f "$HS_CONFIG_FOLDER"/nozzle-cleaning-fan-control.cfg + if grep -q "include Helper-Script/nozzle-cleaning-fan-control" "$PRINTER_CFG" ; then + echo -e "Info: Removing Nozzle Cleaning Fan Control configurations in printer.cfg file..." + sed -i '/include Helper-Script\/nozzle-cleaning-fan-control\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Nozzle Cleaning Fan Control configurations are already removed in printer.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Nozzle Cleaning Fan Control has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/octoeverywhere.sh b/scripts/octoeverywhere.sh new file mode 100755 index 0000000..ce23817 --- /dev/null +++ b/scripts/octoeverywhere.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +set -e + +function octoeverywhere_message(){ + top_line + title 'OctoEverywhere' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}Cloud empower your Klipper printers with free, private, and ${white}│" + echo -e " │ ${cyan}unlimited remote access to your full web control portal from ${white}│" + echo -e " │ ${cyan}anywhere! ${white}│" + hr + bottom_line +} + +function install_octoeverywhere(){ + octoeverywhere_message + local yn + while true; do + install_msg "OctoEverywhere" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -d "$OCTOEVERYWHERE_FOLDER" ]; then + echo -e "Info: OctoEverywhere is already installed. Download skipped." + else + echo -e "Info: Downloading OctoEverywhere..." + git config --global http.sslVerify false + git clone "$OCTOEVERYWHERE_URL" "$OCTOEVERYWHERE_FOLDER" + fi + echo -e "Info: Running OctoEverywhere installer..." + cd "$OCTOEVERYWHERE_FOLDER" + sh ./install.sh + ok_msg "OctoEverywhere has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_octoeverywhere(){ + octoeverywhere_message + local yn + while true; do + remove_msg "OctoEverywhere" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Running OctoEverywhere installer..." + cd "$OCTOEVERYWHERE_FOLDER" + sh ./uninstall.sh + ok_msg "OctoEverywhere has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/paths.sh b/scripts/paths.sh new file mode 100755 index 0000000..edeaf0d --- /dev/null +++ b/scripts/paths.sh @@ -0,0 +1,173 @@ +#!/bin/sh + +set -e + +function set_paths() { + + # Colors # + white=`echo -en "\033[m"` + blue=`echo -en "\033[36m"` + cyan=`echo -en "\033[1;36m"` + yellow=`echo -en "\033[1;33m"` + green=`echo -en "\033[01;32m"` + darkred=`echo -en "\033[31m"` + red=`echo -en "\033[01;31m"` + + # System # + CURL="${HELPER_SCRIPT_FOLDER}/files/fixes/curl" + INITD_FOLDER="/etc/init.d" + USR_DATA="/usr/data" + PRINTER_DATA_FOLDER="$USR_DATA/printer_data" + + # Helper Script # + HS_FILES="${HELPER_SCRIPT_FOLDER}/files" + HS_CONFIG_FOLDER="$PRINTER_DATA_FOLDER/config/Helper-Script" + HS_BACKUP_FOLDER="$USR_DATA/helper-script-backup" + + # Configuration Files # + MOONRAKER_CFG="${PRINTER_DATA_FOLDER}/config/moonraker.conf" + PRINTER_CFG="${PRINTER_DATA_FOLDER}/config/printer.cfg" + MACROS_CFG="${PRINTER_DATA_FOLDER}/config/gcode_macro.cfg" + + # Moonraker # + MOONRAKER_FOLDER="${USR_DATA}/moonraker" + MOONRAKER_URL1="${HS_FILES}/moonraker/moonraker.tar.gz" + MOONRAKER_URL2="${HS_FILES}/moonraker/moonraker.conf" + MOONRAKER_URL3="${HS_FILES}/moonraker/moonraker.asvc" + MOONRAKER_SERVICE_URL="${HS_FILES}/services/S56moonraker_service" + + # Nginx # + NGINX_FOLDER="${USR_DATA}/nginx" + NGINX_SERVICE_URL="${HS_FILES}/services/S50nginx" + + # Supervisor Lite # + SUPERVISOR_FILE="/usr/bin/supervisorctl" + SUPERVISOR_URL="${HS_FILES}/fixes/supervisorctl" + + # Host Controls Support # + SYSTEMCTL_FILE="/usr/bin/systemctl" + SYSTEMCTL_URL="${HS_FILES}/fixes/systemctl" + SUDO_FILE="/usr/bin/sudo" + SUDO_URL="${HS_FILES}/fixes/sudo" + + # Klipper # + KLIPPER_EXTRAS_FOLDER="/usr/share/klipper/klippy/extras" + KLIPPER_CONFIG_FOLDER="${PRINTER_DATA_FOLDER}/config" + KLIPPER_KLIPPY_FOLDER="/usr/share/klipper/klippy" + KLIPPER_SERVICE_URL="${HS_FILES}/services/S55klipper_service" + KLIPPER_GCODE_URL="${HS_FILES}/fixes/gcode.py" + + # Fluidd # + FLUIDD_FOLDER="${USR_DATA}/fluidd" + FLUIDD_URL="https://github.com/fluidd-core/fluidd/releases/latest/download/fluidd.zip" + + # Mainsail # + MAINSAIL_FOLDER="${USR_DATA}/mainsail" + MAINSAIL_URL="https://github.com/mainsail-crew/mainsail/releases/latest/download/mainsail.zip" + + # Entware # + ENTWARE_FILE="/opt/bin/opkg" + ENTWARE_URL="${HS_FILES}/entware/generic.sh" + + # Klipper Gcode Shell Command # + KLIPPER_SHELL_FILE="${KLIPPER_EXTRAS_FOLDER}/gcode_shell_command.py" + KLIPPER_SHELL_URL="${HS_FILES}/gcode-shell-command/gcode_shell_command.py" + + # Klipper Adaptive Meshing & Purging # + KAMP_FOLDER="${HS_CONFIG_FOLDER}/KAMP" + KAMP_URL="${HS_FILES}/kamp" + + # Buzzer Support # + BUZZER_FILE="${HS_CONFIG_FOLDER}/buzzer-support.cfg" + BUZZER_URL="${HS_FILES}/buzzer-support/buzzer-support.cfg" + + # Nozzle Cleaning Fan Control # + NOZZLE_CLEANING_FOLDER="${KLIPPER_EXTRAS_FOLDER}/prtouch_v2_fan" + NOZZLE_CLEANING_URL1="${HS_FILES}/nozzle-cleaning-fan-control/__init__.py" + NOZZLE_CLEANING_URL2="${HS_FILES}/nozzle-cleaning-fan-control/prtouch_v2_fan.pyc" + NOZZLE_CLEANING_URL3="${HS_FILES}/nozzle-cleaning-fan-control/nozzle-cleaning-fan-control.cfg" + + # Fans Control Macros # + FAN_CONTROLS_FILE="${HS_CONFIG_FOLDER}/fans-control.cfg" + FAN_CONTROLS_URL="${HS_FILES}/macros/fans-control.cfg" + + # Improved Shapers Calibrations # + IMP_SHAPERS_FOLDER="${HS_CONFIG_FOLDER}/improved-shapers" + IMP_SHAPERS_URL="${HS_FILES}/improved-shapers/" + + # Useful Macros # + USEFUL_MACROS_FILE="${HS_CONFIG_FOLDER}/useful-macros.cfg" + USEFUL_MACROS_URL="${HS_FILES}/macros/useful-macros.cfg" + + # Save Z-Offset Macros # + SAVE_ZOFFSET_FILE="${HS_CONFIG_FOLDER}/save-zoffset.cfg" + SAVE_ZOFFSET_URL="${HS_FILES}/macros/save-zoffset.cfg" + + # Screws Tilt Adjust Support # + SCREWS_ADJUST_FILE="${HS_CONFIG_FOLDER}/screws-tilt-adjust.cfg" + SCREWS_ADJUST_URL="${HS_FILES}/screws-tilt-adjust/screws_tilt_adjust.py" + SCREWS_ADJUST_K1_URL="${HS_FILES}/screws-tilt-adjust/screws-tilt-adjust-k1.cfg" + SCREWS_ADJUST_K1M_URL="${HS_FILES}/screws-tilt-adjust/screws-tilt-adjust-k1max.cfg" + + # Virtual Pins Support # + VIRTUAL_PINS_FILE="${KLIPPER_EXTRAS_FOLDER}/virtual_pins.py" + VIRTUAL_PINS_URL="${HS_FILES}/klipper-virtual-pins/virtual_pins.py" + + # M600 Support # + M600_SUPPORT_FILE="${HS_CONFIG_FOLDER}/M600-support.cfg" + M600_SUPPORT_URL="${HS_FILES}/macros/M600-support.cfg" + + # Git Backup # + GIT_BACKUP_INSTALLER="${HS_FILES}/git-backup/git-backup.sh" + GIT_BACKUP_FILE="${HS_CONFIG_FOLDER}/git-backup.cfg" + GIT_BACKUP_URL="${HS_FILES}/git-backup/git-backup.cfg" + + # Moonraker Timelapse # + TIMELAPSE_FILE="${USR_DATA}/moonraker/moonraker/moonraker/components/timelapse.py" + TIMELAPSE_URL1="${HS_FILES}/moonraker-timelapse/timelapse.py" + TIMELAPSE_URL2="${HS_FILES}/moonraker-timelapse/timelapse.cfg" + + # Camera Settings Control # + CAMERA_SETTINGS_FILE="${HS_CONFIG_FOLDER}/camera-settings.cfg" + CAMERA_SETTINGS_URL="${HS_FILES}/camera-settings/camera-settings.cfg" + + # OctoEverywhere # + OCTOEVERYWHERE_FOLDER="${USR_DATA}/octoeverywhere" + OCTOEVERYWHERE_URL="https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" + + # Moonraker Obico # + MOONRAKER_OBICO_FOLDER="${USR_DATA}/moonraker-obico" + MOONRAKER_OBICO_URL="https://github.com/TheSpaghettiDetective/moonraker-obico.git" + + # Mobileraker Companion # + MOBILERAKER_COMPANION_FOLDER="${USR_DATA}/mobileraker_companion" + MOBILERAKER_COMPANION_URL="https://github.com/Clon1998/mobileraker_companion.git" + + # Custom Boot Display # + BOOT_DISPLAY_FOLDER="/etc/boot-display" + BOOT_DISPLAY_FILE="${BOOT_DISPLAY_FOLDER}/part0/pic_100.jpg" + BOOT_DISPLAY_K1_URL="${HS_FILES}/boot-display/k1_boot_display.tar.gz" + BOOT_DISPLAY_K1M_URL="${HS_FILES}/boot-display/k1max_boot_display.tar.gz" + BOOT_DISPLAY_STOCK_URL="${HS_FILES}/boot-display/stock_boot_display.tar.gz" + + # Creality Web Interface # + CREALITY_WEB_FILE="/usr/bin/web-server" + + # Guppy Screen # + GUPPY_SCREEN_FOLDER="${USR_DATA}/guppyscreen" + GUPPY_SCREEN_URL1="${HS_FILES}/guppy-screen/guppy_update.cfg" + GUPPY_SCREEN_URL2="${HS_FILES}/guppy-screen/guppy-update.sh" + + # Creality Dynamic Logos for Fluidd # + FLUIDD_LOGO_FILE="${USR_DATA}/fluidd/logo_creality_v2.svg" + FLUIDD_LOGO_URL1="${HS_FILES}/fluidd-logos/logo_creality_v1.svg" + FLUIDD_LOGO_URL2="${HS_FILES}/fluidd-logos/logo_creality_v2.svg" + FLUIDD_LOGO_URL3="${HS_FILES}/fluidd-logos/config.json" + +} + +function set_permissions() { + + chmod +x "$CURL" >/dev/null 2>&1 & + +} \ No newline at end of file diff --git a/scripts/save_zoffset_macros.sh b/scripts/save_zoffset_macros.sh new file mode 100755 index 0000000..c71c880 --- /dev/null +++ b/scripts/save_zoffset_macros.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +set -e + +function save_zoffset_macros_message(){ + top_line + title 'Save Z-Offset Macros' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to save and load the the Z-Offset automatically. ${white}│" + hr + bottom_line +} + +function install_save_zoffset_macros(){ + save_zoffset_macros_message + local yn + while true; do + install_msg "Save Z-Offset Macros" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/save-zoffset.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/save-zoffset.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Linking file..." + ln -sf "$SAVE_ZOFFSET_URL" "$HS_CONFIG_FOLDER"/save-zoffset.cfg + if grep -q "include Helper-Script/save-zoffset" "$PRINTER_CFG" ; then + echo -e "Info: Save Z-Offset Macros configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Save Z-Offset Macros configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/save-zoffset\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Save Z-Offset Macros have been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_save_zoffset_macros(){ + save_zoffset_macros_message + local yn + while true; do + remove_msg "Save Z-Offset Macros" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing file..." + rm -f "$HS_CONFIG_FOLDER"/save-zoffset.cfg + rm -f "$HS_CONFIG_FOLDER"/variables.cfg + if grep -q "include Helper-Script/save-zoffset" "$PRINTER_CFG" ; then + echo -e "Info: Removing Save Z-Offset Macros configurations in printer.cfg file..." + sed -i '/include Helper-Script\/save-zoffset\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Save Z-Offset Macros configurations are already removed in printer.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Save Z-Offset Macros have been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/screws_tilt_adjust.sh b/scripts/screws_tilt_adjust.sh new file mode 100755 index 0000000..11e9ec0 --- /dev/null +++ b/scripts/screws_tilt_adjust.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +set -e + +function screws_tilt_adjust_message(){ + top_line + title 'Screws Tilt Adjust Support' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to add support for Screws Tilt Adjust ${white}│" + echo -e " │ ${cyan}functionality. ${white}│" + hr + bottom_line +} + +function install_screws_tilt_adjust(){ + screws_tilt_adjust_message + local yn + while true; do + install_msg "Screws Tilt Adjust Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/screws-tilt-adjust.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/screws-tilt-adjust.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Backing up original file..." + if [ ! -d "$HS_BACKUP_FOLDER"/screws-tilt-adjust ]; then + mkdir -p "$HS_BACKUP_FOLDER"/screws-tilt-adjust + fi + if [ -f "$KLIPPER_EXTRAS_FOLDER"/screws_tilt_adjust.py ]; then + mv "$KLIPPER_EXTRAS_FOLDER"/screws_tilt_adjust.py "$HS_BACKUP_FOLDER"/screws-tilt-adjust + mv "$KLIPPER_EXTRAS_FOLDER"/screws_tilt_adjust.pyc "$HS_BACKUP_FOLDER"/screws-tilt-adjust + fi + echo + local printer_choice + while true; do + read -p " ${white}Do you want install it for ${yellow}K1${white} or ${yellow}K1 Max${white}? (${yellow}k1${white}/${yellow}k1max${white}): ${yellow}" printer_choice + case "${printer_choice}" in + K1|k1) + echo -e "${white}" + echo -e "Info: Linking files..." + ln -sf "$SCREWS_ADJUST_K1_URL" "$HS_CONFIG_FOLDER"/screws-tilt-adjust.cfg + break;; + K1MAX|k1max) + echo -e "${white}" + echo -e "Info: Linking files..." + ln -sf "$SCREWS_ADJUST_K1M_URL" "$HS_CONFIG_FOLDER"/screws-tilt-adjust.cfg + break;; + *) + error_msg "Please select a correct choice!";; + esac + done + ln -sf "$SCREWS_ADJUST_URL" "$KLIPPER_EXTRAS_FOLDER"/screws_tilt_adjust.py + if grep -q "include Helper-Script/screws-tilt-adjust" "$PRINTER_CFG" ; then + echo -e "Info: Screws Tilt Adjust Support configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Screws Tilt Adjust Support configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/screws-tilt-adjust\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Screws Tilt Adjust Support has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_screws_tilt_adjust(){ + screws_tilt_adjust_message + local yn + while true; do + remove_msg "Screws Tilt Adjust Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Restoring files..." + if [ -f "$HS_BACKUP_FOLDER"/screws-tilt-adjust/screws_tilt_adjust.py ]; then + mv "$HS_BACKUP_FOLDER"/screws-tilt-adjust/screws_tilt_adjust.py "$KLIPPER_EXTRAS_FOLDER" + mv "$HS_BACKUP_FOLDER"/screws-tilt-adjust/screws_tilt_adjust.pyc "$KLIPPER_EXTRAS_FOLDER" + rm -rf "$HS_BACKUP_FOLDER"/screws-tilt-adjust + fi + if [ ! -n "$(ls -A "$HS_BACKUP_FOLDER")" ]; then + rm -rf "$HS_BACKUP_FOLDER" + fi + echo -e "Info: Removing file..." + rm -f "$HS_CONFIG_FOLDER"/screws-tilt-adjust.cfg + if grep -q "include Helper-Script/screws-tilt-adjust" "$PRINTER_CFG" ; then + echo -e "Info: Removing Screws Tilt Adjust Support configurations in printer.cfg file..." + sed -i '/include Helper-Script\/screws-tilt-adjust\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Screws Tilt Adjust Support configurations are already removed in printer.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Screws Tilt Adjust Support has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/tools.sh b/scripts/tools.sh new file mode 100755 index 0000000..ea7399a --- /dev/null +++ b/scripts/tools.sh @@ -0,0 +1,330 @@ +#!/bin/sh + +set -e + +function prevent_updating_klipper_files_message(){ + top_line + title 'Prevent updating Klipper configuration files' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This prevents updating Klipper configuration files when ${white}│" + echo -e " │ ${cyan}Klipper restarts. ${white}│" + hr + bottom_line +} + +function allow_updating_klipper_files_message(){ + top_line + title 'Allow updating Klipper configuration files' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows updating Klipper configuration files when ${white}│" + echo -e " │ ${cyan}Klipper restarts. ${white}│" + hr + bottom_line +} + +function printing_gcode_from_folder_message(){ + top_line + title 'Fix printing Gcode files from folder' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}From Fluidd or Mainsail it's possible to classify your Gcode ${white}│" + echo -e " │ ${cyan}files in folders but by default it's not possible to start ${white}│" + echo -e " │ ${cyan}a print from a folder. This fix allows that. ${white}│" + hr + bottom_line +} + +function restore_previous_firmware_message(){ + top_line + title 'Restore a previous firmware' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}To restore a previous firmware, follow these steps and ${white}│" + echo -e " │ ${cyan}validate your choice: ${white}│" + echo -e " │ │" + echo -e " │ ${cyan}1. ${white}Copy the firmware (.img) you want to update to the root ${white}│" + echo -e " │ of a USB drive. ${white}│" + echo -e " │ ${cyan}2. ${white}Make sure there is only this file on the USB drive. ${white}│" + echo -e " │ ${cyan}3. ${white}Insert the USB drive into the printer. ${white}│" + hr + bottom_line +} + +function reset_factory_settings_message(){ + top_line + title 'Reset factory settings' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This the best way to reset the printer to its factory ${white}│" + echo -e " │ ${cyan}settings. ${white}│" + echo -e " │ ${cyan}Note that the Factory Reset function in the screen menu ${white}│" + echo -e " │ ${cyan}settings only performs a partial reset. ${white}│" + hr + echo -e " │ ${cyan}Note: After factory reset all features already been ${white}│" + echo -e " │ ${cyan}installed with Creality Helper Script must be reinstalled ${white}│" + echo -e " │ ${cyan}and it's necessary to reconnect your printer to your network ${white}│" + echo -e " │ ${cyan} from screen settings in `Settings` → `Network` tab. ${white}│" + hr + bottom_line +} + +function prevent_updating_klipper_files(){ + prevent_updating_klipper_files_message + local yn + while true; do + read -p "${white} Do you want to prevent updating ${green}Klipper configuration files ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Backup file..." + mv "$INITD_FOLDER"/S55klipper_service "$INITD_FOLDER"/disabled.S55klipper_service + echo -e "Info: Copying file..." + cp "$KLIPPER_SERVICE_URL" "$INITD_FOLDER"/S55klipper_service + echo -e "Info: Applying permissions..." + chmod 755 "$INITD_FOLDER"/S55klipper_service + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Klipper configuration files will no longer be updated when Klipper restarts!" + return;; + N|n) + error_msg "Preventing canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function allow_updating_klipper_files(){ + allow_updating_klipper_files_message + local yn + while true; do + read -p "${white} Do you want to allow updating ${green}Klipper configuration files ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Restoring file..." + rm -f /etc/init.d/S55klipper_service + mv "$INITD_FOLDER"/disabled.S55klipper_service "$INITD_FOLDER"/S55klipper_service + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Klipper configuration files will be updated when Klipper restarts!" + return;; + N|n) + error_msg "Authorization canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function printing_gcode_from_folder(){ + printing_gcode_from_folder_message + local yn + while true; do + read -p "${white} Do you want to apply fix for ${green}printing Gcode files from folder ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Deleting files..." + if [ -f "$KLIPPER_KLIPPY_FOLDER"/gcode.py ]; then + rm -f "$KLIPPER_KLIPPY_FOLDER"/gcode.py + rm -f "$KLIPPER_KLIPPY_FOLDER"/gcode.pyc + fi + echo -e "Info: Linking files..." + ln -sf "$KLIPPER_GCODE_URL" "$KLIPPER_KLIPPY_FOLDER"/gcode.py + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Fix has been applied successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function restart_nginx_action(){ + echo + local yn + while true; do + restart_msg "Nginx service" yn + case "${yn}" in + Y|y) + echo -e "${white}" + stop_nginx + start_nginx + ok_msg "Nginx service has been restarted successfully!" + return;; + N|n) + error_msg "Restart canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function restart_moonraker_action(){ + echo + local yn + while true; do + restart_msg "Moonraker service" yn + case "${yn}" in + Y|y) + echo -e "${white}" + stop_moonraker + start_moonraker + ok_msg "Moonraker service has been restarted successfully!" + return;; + N|n) + error_msg "Restart canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function restart_klipper_action(){ + echo + local yn + while true; do + restart_msg "Klipper service" yn + case "${yn}" in + Y|y) + echo -e "${white}" + restart_klipper + ok_msg "Klipper service has been restarted successfully!" + return;; + N|n) + error_msg "Restart canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function update_entware_packages(){ + echo + local yn + while true; do + read -p "${white} Are you sure you want to update ${green}Entware packages ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Updating packages list..." + "$ENTWARE_FILE" update + echo -e "Info: Updating packages..." + "$ENTWARE_FILE" upgrade + ok_msg "Entware packages have been updated!" + return;; + N|n) + error_msg "Updating canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function clear_cache(){ + echo + local yn + while true; do + read -p "${white} Are you sure you want to ${green}clear cache ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Clearing root partition cache..." + rm -rf /root/.cache + echo -e "Info: Clearing git cache..." + cd "${HELPER_SCRIPT_FOLDER}" + git gc --aggressive --prune=all + ok_msg "Cache has been cleared!" + return;; + N|n) + error_msg "Clearing cache canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function clear_logs(){ + echo + local yn + while true; do + read -p "${white} Are you sure you want to clear ${green}logs files ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Clearing logs files..." + rm -f "$USR_DATA"/creality/userdata/log/*.log + rm -f "$USR_DATA"/creality/userdata/log/*.gz + rm -f "$USR_DATA"/creality/userdata/fault_code/* + rm -f "$PRINTER_DATA_FOLDER"/logs/* + ok_msg "Logs files have been cleared!" + return;; + N|n) + error_msg "Clearing logs files canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function restore_previous_firmware(){ + restore_previous_firmware_message + local yn + while true; do + read -p "${white} Do you want to restore a previous firmware ? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + if ls /tmp/udisk/sda1/*.img 1> /dev/null 2>&1; then + echo -e "${white}" + echo "Info: Restoring firmware..." + rm -rf /overlay/upper/* + /etc/ota_bin/local_ota_update.sh /tmp/udisk/sda1/*.img + ok_msg "Firmware has been restored! Please reboot your printer." + exit 0 + else + error_msg "No .img file found on the USB drive. Restoration canceled!" + fi + return;; + N|n) + error_msg "Restoration canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function reset_factory_settings(){ + reset_factory_settings_message + local yn + while true; do + read -p "${white} Are you sure you want to ${green}reset factory settings ${white}? (${yellow}y${white}/${yellow}n${white}): ${yellow}" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Restoration..." + echo "all" | nc -U /var/run/wipe.sock + ;; + N|n) + error_msg "Reset canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/useful_macros.sh b/scripts/useful_macros.sh new file mode 100755 index 0000000..fd806dc --- /dev/null +++ b/scripts/useful_macros.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +set -e + +function useful_macros_message(){ + top_line + title 'Useful Macros' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows to use some usefull macros like Bed Leveling, PID, ${white}│" + echo -e " │ ${cyan}stress test or backup and restore Klipper configurations ${white}│" + echo -e " │ ${cyan}files and Moonraker database. ${white}│" + hr + bottom_line +} + +function install_useful_macros(){ + useful_macros_message + local yn + while true; do + install_msg "Useful Macros" yn + case "${yn}" in + Y|y) + echo -e "${white}" + if [ -f "$HS_CONFIG_FOLDER"/useful-macros.cfg ]; then + rm -f "$HS_CONFIG_FOLDER"/useful-macros.cfg + fi + if [ ! -d "$HS_CONFIG_FOLDER" ]; then + mkdir -p "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Linking file..." + ln -sf "$USEFUL_MACROS_URL" "$HS_CONFIG_FOLDER"/useful-macros.cfg + if grep -q "include Helper-Script/useful-macros" "$PRINTER_CFG" ; then + echo -e "Info: Useful Macros configurations are already enabled in printer.cfg file..." + else + echo -e "Info: Adding Useful Macros configurations in printer.cfg file..." + sed -i '/\[include printer_params\.cfg\]/a \[include Helper-Script/useful-macros\.cfg\]' "$PRINTER_CFG" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Useful Macros have been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_useful_macros(){ + useful_macros_message + local yn + while true; do + remove_msg "Useful Macros" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing file..." + rm -f "$HS_CONFIG_FOLDER"/useful-macros.cfg + if grep -q "include Helper-Script/useful-macros" "$PRINTER_CFG" ; then + echo -e "Info: Removing Useful Macros configurations in printer.cfg file..." + sed -i '/include Helper-Script\/useful-macros\.cfg/d' "$PRINTER_CFG" + else + echo -e "Info: Useful Macros configurations are already removed in printer.cfg file..." + fi + if [ ! -n "$(ls -A "$HS_CONFIG_FOLDER")" ]; then + rm -rf "$HS_CONFIG_FOLDER" + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Useful Macros have been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file diff --git a/scripts/virtual_pins.sh b/scripts/virtual_pins.sh new file mode 100755 index 0000000..096f1f4 --- /dev/null +++ b/scripts/virtual_pins.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +set -e + +function virtual_pins_message(){ + top_line + title 'Virtual Pins Support' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}It allows usage of virtual (simulated) pins in Klipper ${white}│" + echo -e " │ ${cyan}configurations files. ${white}│" + hr + bottom_line +} + +function install_virtual_pins(){ + virtual_pins_message + local yn + while true; do + install_msg "Virtual Pins Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Linking file..." + ln -sf "$VIRTUAL_PINS_URL" "$VIRTUAL_PINS_FILE" + if grep -q "[virtual_pins]" "$PRINTER_CFG" ; then + echo -e "Info: Adding [virtual_pins] configuration in printer.cfg file..." + sed -i '/\[include sensorless.cfg\]/i [virtual_pins]' "$PRINTER_CFG" + else + echo -e "Info: [virtual_pins] configuration is already enabled in printer.cfg file..." + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Virtual Pins Support has been installed successfully!" + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_virtual_pins(){ + virtual_pins_message + local yn + while true; do + remove_msg "Virtual Pins Support" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Removing file..." + rm -f "$KLIPPER_EXTRAS_FOLDER"/virtual_pins.py + rm -f "$KLIPPER_EXTRAS_FOLDER"/virtual_pins.pyc + if grep -q "[virtual_pins]" "$PRINTER_CFG" ; then + echo -e "Info: Removing [virtual_pins] configuration in printer.cfg file..." + sed -i '/\[virtual_pins\]/d' "$PRINTER_CFG" + else + echo -e "Info: [virtual_pins] configuration is already removed in printer.cfg file..." + fi + echo -e "Info: Restarting Klipper service..." + restart_klipper + ok_msg "Virtual Pins Support has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} \ No newline at end of file