From f843e2d09a83f57674708aae9cbf352366fe442c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 10 Feb 2016 13:25:58 +0100 Subject: [PATCH 001/438] Initial commit --- .gitignore | 62 +++++ LICENSE | 661 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 725 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..1dbc687de0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..dbbe355815 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..e0573ec934 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# JediWifiPrintingPlugin +Secret plugin to enable wifi printing from Cura to JediPrinter From 763206af3247999015f321301e9074e83c21b136 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 10 Feb 2016 13:29:17 +0100 Subject: [PATCH 002/438] Initial commit --- HttpUploadDataStream.py | 32 +++++++++ WifiConnection.py | 142 ++++++++++++++++++++++++++++++++++++++ WifiOutputDevicePlugin.py | 53 ++++++++++++++ __init__.py | 20 ++++++ 4 files changed, 247 insertions(+) create mode 100644 HttpUploadDataStream.py create mode 100644 WifiConnection.py create mode 100644 WifiOutputDevicePlugin.py create mode 100644 __init__.py diff --git a/HttpUploadDataStream.py b/HttpUploadDataStream.py new file mode 100644 index 0000000000..a3e5a82ba4 --- /dev/null +++ b/HttpUploadDataStream.py @@ -0,0 +1,32 @@ +from UM.Signal import Signal, SignalEmitter +class HttpUploadDataStream(SignalEmitter): + def __init__(self): + super().__init__() + self._data_list = [] + self._total_length = 0 + self._read_position = 0 + + progressSignal = Signal() + + def write(self, data): + data = bytes(data,'UTF-8') + size = len(data) + if size < 1: + return + blocks = int(size / 2048) + for n in range(0, blocks): + self._data_list.append(data[n*2048:n*2048+2048]) + self._data_list.append(data[blocks*2048:]) + self._total_length += size + + def read(self, size): + if self._read_position >= len(self._data_list): + return None + ret = self._data_list[self._read_position] + self._read_position += 1 + + self.progressSignal.emit(float(self._read_position) / float(len(self._data_list))) + return ret + + def __len__(self): + return self._total_length \ No newline at end of file diff --git a/WifiConnection.py b/WifiConnection.py new file mode 100644 index 0000000000..5d1a4a6c9c --- /dev/null +++ b/WifiConnection.py @@ -0,0 +1,142 @@ +from UM.OutputDevice.OutputDevice import OutputDevice +from UM.OutputDevice import OutputDeviceError +import threading +import http.client as httpclient +import urllib +import json +import time +import base64 + +from . import HttpUploadDataStream +from UM.i18n import i18nCatalog +from UM.Signal import Signal, SignalEmitter +from UM.Application import Application +from UM.Logger import Logger +i18n_catalog = i18nCatalog("cura") + +class WifiConnection(OutputDevice, SignalEmitter): + def __init__(self, address, info): + super().__init__(address) + self._address = address + self._info = info + self._http_lock = threading.Lock() + self._http_connection = None + self._file = None + self._do_update = True + self._thread = None + self._state = None + self._is_connected = False + self.connect() + self.setName(address) + self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with WIFI")) + self.setDescription(i18n_catalog.i18nc("@info:tooltip", "Print with WIFI")) + self.setIconName("print") + + connectionStateChanged = Signal() + + def isConnected(self): + return self._is_connected + + def setConnectionState(self, state): + print("setting connection state: " , self._address, " " , state) + if state != self._is_connected: + self._is_connected = state + self.connectionStateChanged.emit(self._address) + else: + self._is_connected = state + + def _update(self): + while self._thread: + state_reply = self._httpRequest('GET', '/api/v1/printer/state') + if state_reply is not None: + self._state = state_reply + if not self._is_connected: + self.setConnectionState(True) + else: + self._state = {'state': 'CONNECTION_ERROR'} + self.setConnectionState(False) + time.sleep(1) + + def close(self): + self._do_update = False + self._is_connected = False + + def requestWrite(self, node): + self._file = getattr( Application.getInstance().getController().getScene(), "gcode_list") + self.startPrint() + + #Open the active connection to the printer so we can send commands + def connect(self): + if self._thread is None: + self._do_update = True + self._thread = threading.Thread(target = self._update) + self._thread.daemon = True + self._thread.start() + + def getCameraImage(self): + pass #Do Nothing + + def startPrint(self): + try: + result = self._httpRequest('POST', '/api/v1/printer/print_upload', {'print_name': 'Print from Cura', 'parameters':''}, {'file': ('file.gcode', self._file)}) + print(result.get('success',False)) + #result = self._httpRequest('POST', '/api/v1/printer/print_upload', {'print_name': 'Print from Cura'}) + except Exception as e: + Logger.log('e' , 'An exception occured in wifi connection: ' , e) + + def _httpRequest(self, method, path, post_data = None, files = None): + with self._http_lock: + self._http_connection = httpclient.HTTPConnection(self._address, timeout = 30) + try: + if files is not None: + boundary = 'wL36Yn8afVp8Ag7AmP8qZ0SA4n1v9T' + s = HttpUploadDataStream.HttpUploadDataStream() + for k, v in files.items(): + filename = v[0] + file_contents = v[1] + s.write('--%s\r\n' % (boundary)) + s.write('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (k, filename)) + s.write('Content-Type: application/octet-stream\r\n') + s.write('Content-Transfer-Encoding: binary\r\n') + s.write('\r\n') + + if file_contents is not None: + for line in file_contents: + s.write(str(line)) + + s.write('\r\n') + + for k, v in post_data.items(): + s.write('--%s\r\n' % (boundary)) + s.write('Content-Disposition: form-data; name="%s"\r\n' % (k)) + s.write('\r\n') + s.write(str(v)) + s.write('\r\n') + s.write('--%s--\r\n' % (boundary)) + + self._http_connection.request(method, path, s, {"Content-type": "multipart/form-data; boundary=%s" % (boundary), "Content-Length": len(s)}) + elif post_data is not None: + + self._http_connection.request(method, path, urllib.urlencode(post_data), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"}) + else: + self._http_connection.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"}) + except IOError: + self._http_connection.close() + return None + except Exception as e: + self._http_connection.close() + return None + + try: + response = self._http_connection.getresponse() + response_text = response.read() + except IOError: + self._http_connection.close() + return None + try: + response = json.loads(response_text.decode("utf-8")) + except ValueError: + self._http_connection.close() + return None + self._http_connection.close() + return response diff --git a/WifiOutputDevicePlugin.py b/WifiOutputDevicePlugin.py new file mode 100644 index 0000000000..e01c078cde --- /dev/null +++ b/WifiOutputDevicePlugin.py @@ -0,0 +1,53 @@ +from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin +from . import WifiConnection + +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange +from UM.Signal import Signal, SignalEmitter + +class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): + def __init__(self): + super().__init__() + self._zero_conf = Zeroconf() + self._browser = None + self._connections = {} + self.addConnectionSignal.connect(self.addConnection) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal. + + addConnectionSignal = Signal() + + def start(self): + self._browser = ServiceBrowser(Zeroconf(), u'_ultimaker._tcp.local.', [self._onServiceChanged]) + + def stop(self): + self._zero_conf.close() + + ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. + def addConnection(self, name, address, properties): + connection = WifiConnection.WifiConnection(address, properties) + connection.connect() + self._connections[address] = connection + if address == "10.180.1.23": #DEBUG + #if address == "10.180.0.249": #DEBUG + connection.startPrint() + connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + + def _onPrinterConnectionStateChanged(self, address): + print(self._connections[address].isConnected()) + if self._connections[address].isConnected(): + self.getOutputDeviceManager().addOutputDevice(self._connections[address]) + else: + self.getOutputDeviceManager().removeOutputDevice(self._connections[address]) + + def removeConnection(self): + pass + + def _onServiceChanged(self, zeroconf, service_type, name, state_change): + if state_change == ServiceStateChange.Added: + info = zeroconf.get_service_info(service_type, name) + if info: + if info.properties.get(b"type", None): + address = '.'.join(map(lambda n: str(n), info.address)) + self.addConnectionSignal.emit(str(name), address, info.properties) + + elif state_change == ServiceStateChange.Removed: + print("Device disconnected") + #print("HERP DERP") diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..f06ca20d57 --- /dev/null +++ b/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Cura is released under the terms of the AGPLv3 or higher. +from . import WifiOutputDevicePlugin + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + +def getMetaData(): + return { + "plugin": { + "name": "Wifi connection", + "author": "Ultimaker", + "description": catalog.i18nc("Wifi connection", "Wifi connection"), + "api": 2 + } + } + +def register(app): + return { "output_device": WifiOutputDevicePlugin.WifiOutputDevicePlugin() } + From ca502705c24a34a7659ebf1d0ae2520c077d4235 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 11 Feb 2016 11:14:18 +0100 Subject: [PATCH 003/438] Reworked wifi connection to use requests instead of httpclient This library is far easier to use and much cleaner. CURA-49 --- WifiConnection.py | 101 ++++++++++++-------------------------- WifiOutputDevicePlugin.py | 19 +++---- 2 files changed, 42 insertions(+), 78 deletions(-) diff --git a/WifiConnection.py b/WifiConnection.py index 5d1a4a6c9c..535971e840 100644 --- a/WifiConnection.py +++ b/WifiConnection.py @@ -1,11 +1,10 @@ from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice import OutputDeviceError import threading -import http.client as httpclient -import urllib import json import time import base64 +import requests from . import HttpUploadDataStream from UM.i18n import i18nCatalog @@ -24,8 +23,13 @@ class WifiConnection(OutputDevice, SignalEmitter): self._file = None self._do_update = True self._thread = None - self._state = None + + self._json_printer_state = None + self._is_connected = False + + self._api_version = "1" + self._api_prefix = "/api/v" + self._api_version + "/" self.connect() self.setName(address) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with WIFI")) @@ -37,23 +41,25 @@ class WifiConnection(OutputDevice, SignalEmitter): def isConnected(self): return self._is_connected + ## Set the connection state of this connection. + # Although we use a restfull API, we do poll the api to check if the machine is still responding. def setConnectionState(self, state): - print("setting connection state: " , self._address, " " , state) if state != self._is_connected: + Logger.log("i", "setting connection state of %s to %s " %(self._address, state)) self._is_connected = state self.connectionStateChanged.emit(self._address) else: - self._is_connected = state + self._is_connected = state def _update(self): while self._thread: - state_reply = self._httpRequest('GET', '/api/v1/printer/state') - if state_reply is not None: - self._state = state_reply + self.setConnectionState(True) + reply = self._httpGet("printer") + if reply.status_code == 200: + self._json_printer_state = reply.json() if not self._is_connected: self.setConnectionState(True) else: - self._state = {'state': 'CONNECTION_ERROR'} self.setConnectionState(False) time.sleep(1) @@ -61,11 +67,11 @@ class WifiConnection(OutputDevice, SignalEmitter): self._do_update = False self._is_connected = False - def requestWrite(self, node): + def requestWrite(self, node, file_name = None): self._file = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() - #Open the active connection to the printer so we can send commands + ## Start the polling thread. def connect(self): if self._thread is None: self._do_update = True @@ -78,65 +84,22 @@ class WifiConnection(OutputDevice, SignalEmitter): def startPrint(self): try: - result = self._httpRequest('POST', '/api/v1/printer/print_upload', {'print_name': 'Print from Cura', 'parameters':''}, {'file': ('file.gcode', self._file)}) - print(result.get('success',False)) - #result = self._httpRequest('POST', '/api/v1/printer/print_upload', {'print_name': 'Print from Cura'}) + result = self._httpPost("print_job", self._file) except Exception as e: - Logger.log('e' , 'An exception occured in wifi connection: ' , e) + Logger.log("e" , "An exception occured in wifi connection: %s" % str(e)) - def _httpRequest(self, method, path, post_data = None, files = None): + def _httpGet(self, path): + return requests.get("http://" + self._address + self._api_prefix + path) + + def _httpPost(self, path, file_data): with self._http_lock: - self._http_connection = httpclient.HTTPConnection(self._address, timeout = 30) - try: - if files is not None: - boundary = 'wL36Yn8afVp8Ag7AmP8qZ0SA4n1v9T' - s = HttpUploadDataStream.HttpUploadDataStream() - for k, v in files.items(): - filename = v[0] - file_contents = v[1] - s.write('--%s\r\n' % (boundary)) - s.write('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (k, filename)) - s.write('Content-Type: application/octet-stream\r\n') - s.write('Content-Transfer-Encoding: binary\r\n') - s.write('\r\n') + files_dict = {} + if isinstance(file_data, list): # in case a list with strings is sent + single_string_file_data = "" + for line in file_data: + single_string_file_data += line + files_dict = {"file":("test.gcode", single_string_file_data)} + else: + files_dict = {"file":("test.gcode", file_data)} - if file_contents is not None: - for line in file_contents: - s.write(str(line)) - - s.write('\r\n') - - for k, v in post_data.items(): - s.write('--%s\r\n' % (boundary)) - s.write('Content-Disposition: form-data; name="%s"\r\n' % (k)) - s.write('\r\n') - s.write(str(v)) - s.write('\r\n') - s.write('--%s--\r\n' % (boundary)) - - self._http_connection.request(method, path, s, {"Content-type": "multipart/form-data; boundary=%s" % (boundary), "Content-Length": len(s)}) - elif post_data is not None: - - self._http_connection.request(method, path, urllib.urlencode(post_data), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"}) - else: - self._http_connection.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"}) - except IOError: - self._http_connection.close() - return None - except Exception as e: - self._http_connection.close() - return None - - try: - response = self._http_connection.getresponse() - response_text = response.read() - except IOError: - self._http_connection.close() - return None - try: - response = json.loads(response_text.decode("utf-8")) - except ValueError: - self._http_connection.close() - return None - self._http_connection.close() - return response + return requests.post("http://" + self._address + self._api_prefix + path, files = files_dict) \ No newline at end of file diff --git a/WifiOutputDevicePlugin.py b/WifiOutputDevicePlugin.py index e01c078cde..e9b1848138 100644 --- a/WifiOutputDevicePlugin.py +++ b/WifiOutputDevicePlugin.py @@ -14,24 +14,24 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): addConnectionSignal = Signal() + ## Start looking for devices on network. def start(self): self._browser = ServiceBrowser(Zeroconf(), u'_ultimaker._tcp.local.', [self._onServiceChanged]) + ## Stop looking for devices on network. def stop(self): self._zero_conf.close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addConnection(self, name, address, properties): - connection = WifiConnection.WifiConnection(address, properties) - connection.connect() - self._connections[address] = connection - if address == "10.180.1.23": #DEBUG - #if address == "10.180.0.249": #DEBUG - connection.startPrint() + if address == "10.180.1.30": #DEBUG + connection = WifiConnection.WifiConnection(address, properties) + connection.connect() + self._connections[address] = connection connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) def _onPrinterConnectionStateChanged(self, address): - print(self._connections[address].isConnected()) + print("_onPrinterConnectionStateChanged" , self._connections[address].isConnected()) if self._connections[address].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._connections[address]) else: @@ -49,5 +49,6 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self.addConnectionSignal.emit(str(name), address, info.properties) elif state_change == ServiceStateChange.Removed: - print("Device disconnected") - #print("HERP DERP") + info = zeroconf.get_service_info(service_type, name) + address = '.'.join(map(lambda n: str(n), info.address)) + print("Device disconnected: ", address) From 69a9ef4a64dca2c12338942f931a8d0e644c5a07 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 11 Feb 2016 11:18:27 +0100 Subject: [PATCH 004/438] Removed debug prints CURA-49 --- WifiOutputDevicePlugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/WifiOutputDevicePlugin.py b/WifiOutputDevicePlugin.py index e9b1848138..f4294c094c 100644 --- a/WifiOutputDevicePlugin.py +++ b/WifiOutputDevicePlugin.py @@ -31,7 +31,6 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) def _onPrinterConnectionStateChanged(self, address): - print("_onPrinterConnectionStateChanged" , self._connections[address].isConnected()) if self._connections[address].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._connections[address]) else: @@ -51,4 +50,3 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): elif state_change == ServiceStateChange.Removed: info = zeroconf.get_service_info(service_type, name) address = '.'.join(map(lambda n: str(n), info.address)) - print("Device disconnected: ", address) From 10b39c5ca4db83bc0cc424f5c2ddd9892d554273 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 11 Feb 2016 15:51:40 +0100 Subject: [PATCH 005/438] Added keys to wificonnection so they can be linked to machine instances CURA-49 --- WifiConnection.py | 26 ++++++++++++++++---------- WifiOutputDevicePlugin.py | 23 +++++++++++++++++------ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/WifiConnection.py b/WifiConnection.py index 535971e840..996cb2e5ea 100644 --- a/WifiConnection.py +++ b/WifiConnection.py @@ -14,9 +14,10 @@ from UM.Logger import Logger i18n_catalog = i18nCatalog("cura") class WifiConnection(OutputDevice, SignalEmitter): - def __init__(self, address, info): - super().__init__(address) + def __init__(self, key, address, info): + super().__init__(key) self._address = address + self._key = key self._info = info self._http_lock = threading.Lock() self._http_connection = None @@ -30,14 +31,16 @@ class WifiConnection(OutputDevice, SignalEmitter): self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" - self.connect() - self.setName(address) + self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with WIFI")) self.setDescription(i18n_catalog.i18nc("@info:tooltip", "Print with WIFI")) self.setIconName("print") connectionStateChanged = Signal() + def getKey(self): + return self._key + def isConnected(self): return self._is_connected @@ -54,12 +57,15 @@ class WifiConnection(OutputDevice, SignalEmitter): def _update(self): while self._thread: self.setConnectionState(True) - reply = self._httpGet("printer") - if reply.status_code == 200: - self._json_printer_state = reply.json() - if not self._is_connected: - self.setConnectionState(True) - else: + try: + reply = self._httpGet("printer") + if reply.status_code == 200: + self._json_printer_state = reply.json() + if not self._is_connected: + self.setConnectionState(True) + else: + self.setConnectionState(False) + except: self.setConnectionState(False) time.sleep(1) diff --git a/WifiOutputDevicePlugin.py b/WifiOutputDevicePlugin.py index f4294c094c..7b89002a43 100644 --- a/WifiOutputDevicePlugin.py +++ b/WifiOutputDevicePlugin.py @@ -3,6 +3,7 @@ from . import WifiConnection from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange from UM.Signal import Signal, SignalEmitter +from UM.Application import Application class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def __init__(self): @@ -11,7 +12,7 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self._browser = None self._connections = {} self.addConnectionSignal.connect(self.addConnection) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal. - + Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineInstanceChanged) addConnectionSignal = Signal() ## Start looking for devices on network. @@ -22,13 +23,23 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def stop(self): self._zero_conf.close() + def _onActiveMachineInstanceChanged(self): + active_machine_key = Application.getInstance().getMachineManager().getActiveMachineInstance().getKey() + for address in self._connections: + if self._connections[address].getKey() == active_machine_key: + self._connections[address].connect() + self._connections[address].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + else: + self._connections[address].close() + print("on active machine instance changed" , active_machine_key) + ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addConnection(self, name, address, properties): - if address == "10.180.1.30": #DEBUG - connection = WifiConnection.WifiConnection(address, properties) - connection.connect() - self._connections[address] = connection - connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + connection = WifiConnection.WifiConnection(name, address, properties) + self._connections[address] = connection + if connection.getKey() == Application.getInstance().getMachineManager().getActiveMachineInstance().getKey(): + self._connections[address].connect() + connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) def _onPrinterConnectionStateChanged(self, address): if self._connections[address].isConnected(): From 3bd25e5f7ff82e1ca167f464c8561a7f42296ade Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 18 Apr 2016 16:16:59 +0200 Subject: [PATCH 006/438] NetworkPrinterOutputDevice now uses reworked printerOutputDevice API CURA-49 --- ...ection.py => NetworkPrinterOutputDevice.py | 41 +++++-------------- WifiOutputDevicePlugin.py | 7 ++-- 2 files changed, 14 insertions(+), 34 deletions(-) rename WifiConnection.py => NetworkPrinterOutputDevice.py (69%) diff --git a/WifiConnection.py b/NetworkPrinterOutputDevice.py similarity index 69% rename from WifiConnection.py rename to NetworkPrinterOutputDevice.py index 996cb2e5ea..de5a5961fd 100644 --- a/WifiConnection.py +++ b/NetworkPrinterOutputDevice.py @@ -1,19 +1,18 @@ -from UM.OutputDevice.OutputDevice import OutputDevice -from UM.OutputDevice import OutputDeviceError import threading -import json import time -import base64 import requests -from . import HttpUploadDataStream from UM.i18n import i18nCatalog -from UM.Signal import Signal, SignalEmitter +from UM.Signal import Signal from UM.Application import Application from UM.Logger import Logger + +from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState + i18n_catalog = i18nCatalog("cura") -class WifiConnection(OutputDevice, SignalEmitter): + +class NetworkPrinterOutputDevice(PrinterOutputDevice): def __init__(self, key, address, info): super().__init__(key) self._address = address @@ -27,8 +26,6 @@ class WifiConnection(OutputDevice, SignalEmitter): self._json_printer_state = None - self._is_connected = False - self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" self.setName(key) @@ -36,38 +33,20 @@ class WifiConnection(OutputDevice, SignalEmitter): self.setDescription(i18n_catalog.i18nc("@info:tooltip", "Print with WIFI")) self.setIconName("print") - connectionStateChanged = Signal() - def getKey(self): return self._key - def isConnected(self): - return self._is_connected - - ## Set the connection state of this connection. - # Although we use a restfull API, we do poll the api to check if the machine is still responding. - def setConnectionState(self, state): - if state != self._is_connected: - Logger.log("i", "setting connection state of %s to %s " %(self._address, state)) - self._is_connected = state - self.connectionStateChanged.emit(self._address) - else: - self._is_connected = state - def _update(self): - while self._thread: - self.setConnectionState(True) + while self._connection_state == ConnectionState.connected or self._connection_state == ConnectionState.busy: try: reply = self._httpGet("printer") if reply.status_code == 200: self._json_printer_state = reply.json() - if not self._is_connected: - self.setConnectionState(True) else: - self.setConnectionState(False) + self.setConnectionState(ConnectionState.error) except: - self.setConnectionState(False) - time.sleep(1) + self.setConnectionState(ConnectionState.error) + time.sleep(1) # Poll every second for printer state. def close(self): self._do_update = False diff --git a/WifiOutputDevicePlugin.py b/WifiOutputDevicePlugin.py index 7b89002a43..8d4ea516e8 100644 --- a/WifiOutputDevicePlugin.py +++ b/WifiOutputDevicePlugin.py @@ -1,5 +1,5 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin -from . import WifiConnection +from . import NetworkPrinterOutputDevice from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange from UM.Signal import Signal, SignalEmitter @@ -35,7 +35,7 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addConnection(self, name, address, properties): - connection = WifiConnection.WifiConnection(name, address, properties) + connection = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) self._connections[address] = connection if connection.getKey() == Application.getInstance().getMachineManager().getActiveMachineInstance().getKey(): self._connections[address].connect() @@ -60,4 +60,5 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): elif state_change == ServiceStateChange.Removed: info = zeroconf.get_service_info(service_type, name) - address = '.'.join(map(lambda n: str(n), info.address)) + if info: + address = '.'.join(map(lambda n: str(n), info.address)) From 960ac9af9961932f7b95dd284d917b1863265a3d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 18 Apr 2016 16:31:14 +0200 Subject: [PATCH 007/438] Close now uses new API stuff --- NetworkPrinterOutputDevice.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index de5a5961fd..e67826eff5 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -49,11 +49,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): time.sleep(1) # Poll every second for printer state. def close(self): - self._do_update = False - self._is_connected = False + self._connection_state == ConnectionState.closed + self._thread = None def requestWrite(self, node, file_name = None): - self._file = getattr( Application.getInstance().getController().getScene(), "gcode_list") + self._file = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() ## Start the polling thread. @@ -79,7 +79,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _httpPost(self, path, file_data): with self._http_lock: files_dict = {} - if isinstance(file_data, list): # in case a list with strings is sent + if isinstance(file_data, list): # in case a list with strings is sent single_string_file_data = "" for line in file_data: single_string_file_data += line From 4817c05a522634518d9e877c948b13f6fc85a786 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 18 Apr 2016 16:32:02 +0200 Subject: [PATCH 008/438] Renamed plugin --- ...tputDevicePlugin.py => NetworkPrinterOutputDevicePlugin.py | 0 __init__.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename WifiOutputDevicePlugin.py => NetworkPrinterOutputDevicePlugin.py (100%) diff --git a/WifiOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py similarity index 100% rename from WifiOutputDevicePlugin.py rename to NetworkPrinterOutputDevicePlugin.py diff --git a/__init__.py b/__init__.py index f06ca20d57..6c3a0c3cc1 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -from . import WifiOutputDevicePlugin +from . import NetworkPrinterOutputDevicePlugin from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -16,5 +16,5 @@ def getMetaData(): } def register(app): - return { "output_device": WifiOutputDevicePlugin.WifiOutputDevicePlugin() } + return { "output_device": NetworkPrinterOutputDevicePlugin.WifiOutputDevicePlugin()} From d621162a9bc62fdf77d0fe1997564aaf38d031c0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 18 Apr 2016 16:36:52 +0200 Subject: [PATCH 009/438] Codestyle --- NetworkPrinterOutputDevicePlugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 8d4ea516e8..5f0352ff3e 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -5,14 +5,18 @@ from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange from UM.Signal import Signal, SignalEmitter from UM.Application import Application + class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def __init__(self): super().__init__() self._zero_conf = Zeroconf() self._browser = None self._connections = {} - self.addConnectionSignal.connect(self.addConnection) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal. + + # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. + self.addConnectionSignal.connect(self.addConnection) Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineInstanceChanged) + addConnectionSignal = Signal() ## Start looking for devices on network. @@ -31,7 +35,6 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self._connections[address].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) else: self._connections[address].close() - print("on active machine instance changed" , active_machine_key) ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addConnection(self, name, address, properties): From 0ad1c1a3a70017fb4e72b89c1f24204e466aab55 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 09:48:21 +0200 Subject: [PATCH 010/438] Renamed connections to printers CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 5f0352ff3e..3b281c3984 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -11,7 +11,7 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): super().__init__() self._zero_conf = Zeroconf() self._browser = None - self._connections = {} + self._printers = {} # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addConnectionSignal.connect(self.addConnection) @@ -29,26 +29,26 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def _onActiveMachineInstanceChanged(self): active_machine_key = Application.getInstance().getMachineManager().getActiveMachineInstance().getKey() - for address in self._connections: - if self._connections[address].getKey() == active_machine_key: - self._connections[address].connect() - self._connections[address].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + for address in self._printers: + if self._printers[address].getKey() == active_machine_key: + self._printers[address].connect() + self._printers[address].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) else: - self._connections[address].close() + self._printers[address].close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addConnection(self, name, address, properties): connection = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) - self._connections[address] = connection + self._printers[address] = connection if connection.getKey() == Application.getInstance().getMachineManager().getActiveMachineInstance().getKey(): - self._connections[address].connect() + self._printers[address].connect() connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) def _onPrinterConnectionStateChanged(self, address): - if self._connections[address].isConnected(): - self.getOutputDeviceManager().addOutputDevice(self._connections[address]) + if self._printers[address].isConnected(): + self.getOutputDeviceManager().addOutputDevice(self._printers[address]) else: - self.getOutputDeviceManager().removeOutputDevice(self._connections[address]) + self.getOutputDeviceManager().removeOutputDevice(self._printers[address]) def removeConnection(self): pass From f488d8eb28cea1156d1793e842ca421c56769cd1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 09:51:32 +0200 Subject: [PATCH 011/438] Zero conf browser now uses stored zeroconf obj CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 3b281c3984..87a0672484 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -21,9 +21,9 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): ## Start looking for devices on network. def start(self): - self._browser = ServiceBrowser(Zeroconf(), u'_ultimaker._tcp.local.', [self._onServiceChanged]) + self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) - ## Stop looking for devices on network. + ## Stop looking for devices on network.s def stop(self): self._zero_conf.close() From 2a90c76cb879aaaa5bbb12b5307fe688f53b2672 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 09:57:31 +0200 Subject: [PATCH 012/438] Renamed connection to printer CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 87a0672484..84f8b89097 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -14,10 +14,10 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self._printers = {} # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. - self.addConnectionSignal.connect(self.addConnection) + self.addPrinterSignal.connect(self.addPrinter) Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineInstanceChanged) - addConnectionSignal = Signal() + addPrinterSignal = Signal() ## Start looking for devices on network. def start(self): @@ -37,12 +37,12 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self._printers[address].close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. - def addConnection(self, name, address, properties): - connection = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) - self._printers[address] = connection - if connection.getKey() == Application.getInstance().getMachineManager().getActiveMachineInstance().getKey(): + def addPrinter(self, name, address, properties): + printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) + self._printers[address] = printer + if printer.getKey() == Application.getInstance().getMachineManager().getActiveMachineInstance().getKey(): self._printers[address].connect() - connection.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) def _onPrinterConnectionStateChanged(self, address): if self._printers[address].isConnected(): @@ -50,7 +50,7 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): else: self.getOutputDeviceManager().removeOutputDevice(self._printers[address]) - def removeConnection(self): + def removePrinter(self): pass def _onServiceChanged(self, zeroconf, service_type, name, state_change): @@ -59,7 +59,7 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): if info: if info.properties.get(b"type", None): address = '.'.join(map(lambda n: str(n), info.address)) - self.addConnectionSignal.emit(str(name), address, info.properties) + self.addPrinterSignal.emit(str(name), address, info.properties) elif state_change == ServiceStateChange.Removed: info = zeroconf.get_service_info(service_type, name) From 404ea89ff778a86a861d641c73422ee328946f3d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 10:51:24 +0200 Subject: [PATCH 013/438] Connection now correctly uses connection state CURA-49 --- NetworkPrinterOutputDevice.py | 21 ++++++++++++++++----- NetworkPrinterOutputDevicePlugin.py | 22 +++++++++++----------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e67826eff5..bd6e8f77f8 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -3,7 +3,6 @@ import time import requests from UM.i18n import i18nCatalog -from UM.Signal import Signal from UM.Application import Application from UM.Logger import Logger @@ -11,7 +10,7 @@ from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState i18n_catalog = i18nCatalog("cura") - +## Network connected (wifi / lan) printer that uses the Ultimaker API class NetworkPrinterOutputDevice(PrinterOutputDevice): def __init__(self, key, address, info): super().__init__(key) @@ -21,7 +20,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._http_lock = threading.Lock() self._http_connection = None self._file = None - self._do_update = True self._thread = None self._json_printer_state = None @@ -37,16 +35,26 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return self._key def _update(self): - while self._connection_state == ConnectionState.connected or self._connection_state == ConnectionState.busy: + Logger.log("d", "Update thread of printer with key %s and ip %s started", self._key, self._address) + while self.isConnected(): try: reply = self._httpGet("printer") if reply.status_code == 200: self._json_printer_state = reply.json() + if self._connection_state == ConnectionState.connecting: + # First successful response, so we are now "connected" + self.setConnectionState(ConnectionState.connected) else: self.setConnectionState(ConnectionState.error) except: self.setConnectionState(ConnectionState.error) time.sleep(1) # Poll every second for printer state. + Logger.log("d", "Update thread of printer with key %s and ip %s stopped", self._key, self._address) + + ## Convenience function that gets information from the recieved json data and converts it to the right internal + # values / variables + def _spliceJsonData(self): + pass def close(self): self._connection_state == ConnectionState.closed @@ -56,10 +64,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._file = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() + def isConnected(self): + return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error + ## Start the polling thread. def connect(self): if self._thread is None: - self._do_update = True + self.setConnectionState(ConnectionState.connecting) self._thread = threading.Thread(target = self._update) self._thread.daemon = True self._thread.start() diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 84f8b89097..cd8d6a3742 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -29,26 +29,26 @@ class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def _onActiveMachineInstanceChanged(self): active_machine_key = Application.getInstance().getMachineManager().getActiveMachineInstance().getKey() - for address in self._printers: - if self._printers[address].getKey() == active_machine_key: - self._printers[address].connect() - self._printers[address].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + for key in self._printers: + if key == active_machine_key: + self._printers[key].connect() + self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) else: - self._printers[address].close() + self._printers[key].close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addPrinter(self, name, address, properties): printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) - self._printers[address] = printer + self._printers[printer.getKey()] = printer if printer.getKey() == Application.getInstance().getMachineManager().getActiveMachineInstance().getKey(): - self._printers[address].connect() + self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) - def _onPrinterConnectionStateChanged(self, address): - if self._printers[address].isConnected(): - self.getOutputDeviceManager().addOutputDevice(self._printers[address]) + def _onPrinterConnectionStateChanged(self, key): + if self._printers[key].isConnected(): + self.getOutputDeviceManager().addOutputDevice(self._printers[key]) else: - self.getOutputDeviceManager().removeOutputDevice(self._printers[address]) + self.getOutputDeviceManager().removeOutputDevice(self._printers[key]) def removePrinter(self): pass From 48625ed129d190eecedba19afe7f2a10f0d6aaf1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 11:02:55 +0200 Subject: [PATCH 014/438] Improved error logging & handling in update thread CURA-49 --- NetworkPrinterOutputDevice.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index bd6e8f77f8..37964254ce 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -24,6 +24,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._json_printer_state = None + self._num_extruders = 2 + self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" self.setName(key) @@ -41,19 +43,26 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): reply = self._httpGet("printer") if reply.status_code == 200: self._json_printer_state = reply.json() + try: + self._spliceJSONData() + except: + # issues with json parsing should not break by definition TODO: Check in what cases it should fail. + pass if self._connection_state == ConnectionState.connecting: # First successful response, so we are now "connected" self.setConnectionState(ConnectionState.connected) else: + Logger.log("w", "Got http status code %s while trying to request data.", reply.status_code) self.setConnectionState(ConnectionState.error) - except: + except Exception as e: self.setConnectionState(ConnectionState.error) - time.sleep(1) # Poll every second for printer state. - Logger.log("d", "Update thread of printer with key %s and ip %s stopped", self._key, self._address) + Logger.log("w", "Exception occured while connecting; %s", str(e)) + time.sleep(2) # Poll every second for printer state. + Logger.log("d", "Update thread of printer with key %s and ip %s stopped with state: %s", self._key, self._address, self._connection_state) ## Convenience function that gets information from the recieved json data and converts it to the right internal # values / variables - def _spliceJsonData(self): + def _spliceJSONData(self): pass def close(self): From 298c740abe3b7566ec846237ccd3f7370f0728b7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 11:06:59 +0200 Subject: [PATCH 015/438] Bed & extruder temps are now logged CURA-49 --- NetworkPrinterOutputDevice.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 37964254ce..2c8e7acb20 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -63,7 +63,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Convenience function that gets information from the recieved json data and converts it to the right internal # values / variables def _spliceJSONData(self): - pass + # Check for hotend temperatures + for index in range(0, self._num_extruders - 1): + temperature = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["temperature"]["current"] + self._setHotendTemperature(index, temperature) + + bed_temperature = self._json_printer_state["bed"]["temperature"] + self._setBedTemperature(bed_temperature) def close(self): self._connection_state == ConnectionState.closed From cf061b2fbef41ffda1a0a01a8f4f239bd6005b05 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 11:42:39 +0200 Subject: [PATCH 016/438] Added filter machines to requestWrite CURA-49 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2c8e7acb20..989b4996f9 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -75,7 +75,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_state == ConnectionState.closed self._thread = None - def requestWrite(self, node, file_name = None): + def requestWrite(self, node, file_name = None, filter_by_machine = False): self._file = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() From 94ed8c8177889b8aa3a1d30bafce94fb7655bc37 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 11:44:51 +0200 Subject: [PATCH 017/438] UpdateThread now correctly uses join upon close CURA-49 --- NetworkPrinterOutputDevice.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 989b4996f9..84018f0a91 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -20,7 +20,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._http_lock = threading.Lock() self._http_connection = None self._file = None - self._thread = None + self._update_thread = None self._json_printer_state = None @@ -73,7 +73,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): self._connection_state == ConnectionState.closed - self._thread = None + self._update_thread.join() + self._update_thread = None def requestWrite(self, node, file_name = None, filter_by_machine = False): self._file = getattr(Application.getInstance().getController().getScene(), "gcode_list") @@ -84,11 +85,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Start the polling thread. def connect(self): - if self._thread is None: + if self._update_thread is None: self.setConnectionState(ConnectionState.connecting) - self._thread = threading.Thread(target = self._update) - self._thread.daemon = True - self._thread.start() + self._update_thread = threading.Thread(target = self._update) + self._update_thread.daemon = True + self._update_thread.start() def getCameraImage(self): pass #Do Nothing From 688ab85dc6676ff7cfe29901536c3b66fd252079 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 11:59:02 +0200 Subject: [PATCH 018/438] Added status messages when printing with network CURA-49 --- NetworkPrinterOutputDevice.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 84018f0a91..a385cdf197 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -6,6 +6,8 @@ from UM.i18n import i18nCatalog from UM.Application import Application from UM.Logger import Logger +from UM.Message import Message + from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState i18n_catalog = i18nCatalog("cura") @@ -33,6 +35,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setDescription(i18n_catalog.i18nc("@info:tooltip", "Print with WIFI")) self.setIconName("print") + self._progress_message = None + self._error_message = None + def getKey(self): return self._key @@ -96,8 +101,19 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def startPrint(self): try: + self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) + self._progress_message.show() + #TODO: Create a job that handles this! (As it currently locks up UI) result = self._httpPost("print_job", self._file) + self._progress_message.hide() + if result.status_code == 200: + pass + except IOError: + self._progress_message.hide() + self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data printer. Is another job still active?")) + self._error_message.show() except Exception as e: + self._progress_message.hide() Logger.log("e" , "An exception occured in wifi connection: %s" % str(e)) def _httpGet(self, path): From 2034aeb5c17e5b624ea41cb3f645ebf5736cae50 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 12:05:03 +0200 Subject: [PATCH 019/438] Progress of a print job is now tracked CURA-49 --- NetworkPrinterOutputDevice.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index a385cdf197..5bcbcea9aa 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -45,9 +45,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("d", "Update thread of printer with key %s and ip %s started", self._key, self._address) while self.isConnected(): try: - reply = self._httpGet("printer") - if reply.status_code == 200: - self._json_printer_state = reply.json() + printer_reply = self._httpGet("printer") + if printer_reply.status_code == 200: + self._json_printer_state = printer_reply.json() try: self._spliceJSONData() except: @@ -57,8 +57,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # First successful response, so we are now "connected" self.setConnectionState(ConnectionState.connected) else: - Logger.log("w", "Got http status code %s while trying to request data.", reply.status_code) + Logger.log("w", "Got http status code %s while trying to request data.", printer_reply.status_code) self.setConnectionState(ConnectionState.error) + + print_job_reply = self._httpGet("print_job") + if print_job_reply.status_code != 404: + self.setProgress(print_job_reply.json()["progress"]) + else: + self.setProgress(0) + + except Exception as e: self.setConnectionState(ConnectionState.error) Logger.log("w", "Exception occured while connecting; %s", str(e)) From 1a44c394e10e341e93dd3ddfed792f7161e4143f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 12:10:53 +0200 Subject: [PATCH 020/438] Warning message is shown if the printer is still active. CURA-49 --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 5bcbcea9aa..f17ea99143 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -108,6 +108,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass #Do Nothing def startPrint(self): + if self._progress != 0: + self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) + self._error_message.show() + return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() @@ -118,7 +122,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass except IOError: self._progress_message.hide() - self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data printer. Is another job still active?")) + self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to printer. Is another job still active?")) self._error_message.show() except Exception as e: self._progress_message.hide() From 387dae140ff50679921c19d60cfb0e5ccb36d35a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 12:15:45 +0200 Subject: [PATCH 021/438] UpdateThread now only joins when it's actually running An output device can be created and never have it's connect called. Because the delete of the output device calls close, we need to handle this CURA-49 --- NetworkPrinterOutputDevice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f17ea99143..21a5e14672 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -65,7 +65,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setProgress(print_job_reply.json()["progress"]) else: self.setProgress(0) - except Exception as e: self.setConnectionState(ConnectionState.error) @@ -86,8 +85,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): self._connection_state == ConnectionState.closed - self._update_thread.join() - self._update_thread = None + if self._update_thread != None: + self._update_thread.join() + self._update_thread = None def requestWrite(self, node, file_name = None, filter_by_machine = False): self._file = getattr(Application.getInstance().getController().getScene(), "gcode_list") From f9bf54348dcea088ec25eb9e037e38284280329d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2016 15:35:56 +0200 Subject: [PATCH 022/438] Head position is now saved CURA-49 --- NetworkPrinterOutputDevice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 21a5e14672..92a68b9d4c 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -83,6 +83,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): bed_temperature = self._json_printer_state["bed"]["temperature"] self._setBedTemperature(bed_temperature) + head_x = self._json_printer_state["heads"][0]["position"]["x"] + head_y = self._json_printer_state["heads"][0]["position"]["y"] + head_z = self._json_printer_state["heads"][0]["position"]["z"] + self._updateHeadPosition(head_x, head_y, head_z) + def close(self): self._connection_state == ConnectionState.closed if self._update_thread != None: From c700a684d3cf8ab6b8dd99097fee4c26b618a931 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 21 Apr 2016 15:57:26 +0200 Subject: [PATCH 023/438] We now use QT stuff for uploading, as this doesn't mess up the GIL CURA-49 --- NetworkPrinterOutputDevice.py | 59 ++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 92a68b9d4c..07e175dced 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -8,8 +8,13 @@ from UM.Logger import Logger from UM.Message import Message +from .SendGCodeJob import SendGCodeJob + from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState +from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager +from PyQt5.QtCore import QUrl + i18n_catalog = i18nCatalog("cura") ## Network connected (wifi / lan) printer that uses the Ultimaker API @@ -35,6 +40,19 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setDescription(i18n_catalog.i18nc("@info:tooltip", "Print with WIFI")) self.setIconName("print") + self._manager = QNetworkAccessManager() + self._manager.finished.connect(self._onFinished) + + ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) + self._qt_request = None + self._qt_reply = None + self._qt_multi_part = None + self._qt_part = None + + + #request_qt_get = QNetworkRequest(QUrl("http://10.180.0.53/api/v1/printer")) + #response = self._manager.get(request_qt_get) + self._progress_message = None self._error_message = None @@ -120,11 +138,34 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() - #TODO: Create a job that handles this! (As it currently locks up UI) - result = self._httpPost("print_job", self._file) - self._progress_message.hide() - if result.status_code == 200: - pass + + single_string_file_data = "" + for line in self._file: + single_string_file_data += line + + ## TODO: Use correct file name (we use placeholder now) + file_name = "test.gcode" + + ## Create multi_part request + self._qt_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) + + ## Create part (to be placed inside multipart) + self._qt_part = QHttpPart() + self._qt_part.setHeader(QNetworkRequest.ContentDispositionHeader, + "form-data; name=\"file\"; filename=\"%s\"" % file_name) + self._qt_part.setBody(single_string_file_data) + self._qt_multi_part.append(self._qt_part) + + url = "http://" + self._address + self._api_prefix + "print_job" + + url_2 = "http://10.180.0.53/api/v1/print_job" + ## Create the QT request + self._qt_request = QNetworkRequest(QUrl("http://10.180.0.53/api/v1/print_job")) + + ## Post request + data + self._qt_reply = self._manager.post(self._qt_request, self._qt_multi_part) + self._qt_reply.uploadProgress.connect(self._onUploadProgress) + except IOError: self._progress_message.hide() self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to printer. Is another job still active?")) @@ -133,6 +174,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.hide() Logger.log("e" , "An exception occured in wifi connection: %s" % str(e)) + def _onFinished(self, reply): + #print(reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) + reply.uploadProgress.disconnect(self._onUploadProgress) + self._progress_message.hide() + + def _onUploadProgress(self, bytes_sent, bytes_total): + self._progress_message.setProgress(bytes_sent / bytes_total * 100) + def _httpGet(self, path): return requests.get("http://" + self._address + self._api_prefix + path) From 27b3fa1d45e4a8382bad60bcbd80a77d841c2f63 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 10:20:46 +0200 Subject: [PATCH 024/438] Removed send-gcode job (no longer need job because we use qt sending) CURA-49 --- NetworkPrinterOutputDevice.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 07e175dced..8defed6bc4 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -8,8 +8,6 @@ from UM.Logger import Logger from UM.Message import Message -from .SendGCodeJob import SendGCodeJob - from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager From bc8f8473e566b57e9d4d848a58728b84780d7961 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 10:22:06 +0200 Subject: [PATCH 025/438] Name refactor CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 2 +- __init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index cd8d6a3742..ac0c55a0b0 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -6,7 +6,7 @@ from UM.Signal import Signal, SignalEmitter from UM.Application import Application -class WifiOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): +class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def __init__(self): super().__init__() self._zero_conf = Zeroconf() diff --git a/__init__.py b/__init__.py index 6c3a0c3cc1..b5aaa03513 100644 --- a/__init__.py +++ b/__init__.py @@ -16,5 +16,5 @@ def getMetaData(): } def register(app): - return { "output_device": NetworkPrinterOutputDevicePlugin.WifiOutputDevicePlugin()} + return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin()} From 41a07c000d9bbe778ca5f5e97d604267bdf4e6f3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 10:24:15 +0200 Subject: [PATCH 026/438] Added extra status catching for when active machine instance is None CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index ac0c55a0b0..b4e2b6a014 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -28,7 +28,11 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self._zero_conf.close() def _onActiveMachineInstanceChanged(self): - active_machine_key = Application.getInstance().getMachineManager().getActiveMachineInstance().getKey() + try: + active_machine_key = Application.getInstance().getMachineManager().getActiveMachineInstance().getKey() + except AttributeError: + ## Active machine instance changed to None. This can happen upon clean start. Simply ignore. + return for key in self._printers: if key == active_machine_key: self._printers[key].connect() From 8b764c7585741575ad24565f3b705dc21bfce54a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 11:39:31 +0200 Subject: [PATCH 027/438] Code cleanup & documentation CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index b4e2b6a014..6e674b289d 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -6,6 +6,9 @@ from UM.Signal import Signal, SignalEmitter from UM.Application import Application +## This plugin handles the connection detection & creation of output device objects for the UM3 printer. +# Zero-Conf is used to detect printers, which are saved in a dict. +# If we discover a printer that has the same key as the active machine instance a connection is made. class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def __init__(self): super().__init__() @@ -23,7 +26,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def start(self): self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) - ## Stop looking for devices on network.s + ## Stop looking for devices on network. def stop(self): self._zero_conf.close() @@ -48,15 +51,14 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + ## Handler for when the connection state of one of the detected printers changes def _onPrinterConnectionStateChanged(self, key): if self._printers[key].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._printers[key]) else: self.getOutputDeviceManager().removeOutputDevice(self._printers[key]) - def removePrinter(self): - pass - + ## Handler for zeroConf detection def _onServiceChanged(self, zeroconf, service_type, name, state_change): if state_change == ServiceStateChange.Added: info = zeroconf.get_service_info(service_type, name) @@ -66,6 +68,6 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): self.addPrinterSignal.emit(str(name), address, info.properties) elif state_change == ServiceStateChange.Removed: - info = zeroconf.get_service_info(service_type, name) - if info: - address = '.'.join(map(lambda n: str(n), info.address)) + pass + # TODO; This isn't testable right now. We need to also decide how to handle + # \ No newline at end of file From 982258ece7b30b7a46cd36521184a9779aae1e6a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 11:42:13 +0200 Subject: [PATCH 028/438] Cleaned up more code CURA-49 --- NetworkPrinterOutputDevice.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 8defed6bc4..6dcc6faf6c 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -15,6 +15,7 @@ from PyQt5.QtCore import QUrl i18n_catalog = i18nCatalog("cura") + ## Network connected (wifi / lan) printer that uses the Ultimaker API class NetworkPrinterOutputDevice(PrinterOutputDevice): def __init__(self, key, address, info): @@ -47,10 +48,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._qt_multi_part = None self._qt_part = None - - #request_qt_get = QNetworkRequest(QUrl("http://10.180.0.53/api/v1/printer")) - #response = self._manager.get(request_qt_get) - self._progress_message = None self._error_message = None @@ -126,7 +123,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_thread.start() def getCameraImage(self): - pass #Do Nothing + pass # TODO: This still needs to be implemented (we don't have a place to show it now anyway) def startPrint(self): if self._progress != 0: @@ -170,10 +167,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message.show() except Exception as e: self._progress_message.hide() - Logger.log("e" , "An exception occured in wifi connection: %s" % str(e)) + Logger.log("e" , "An exception occured in network connection: %s" % str(e)) def _onFinished(self, reply): - #print(reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() @@ -181,17 +177,4 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.setProgress(bytes_sent / bytes_total * 100) def _httpGet(self, path): - return requests.get("http://" + self._address + self._api_prefix + path) - - def _httpPost(self, path, file_data): - with self._http_lock: - files_dict = {} - if isinstance(file_data, list): # in case a list with strings is sent - single_string_file_data = "" - for line in file_data: - single_string_file_data += line - files_dict = {"file":("test.gcode", single_string_file_data)} - else: - files_dict = {"file":("test.gcode", file_data)} - - return requests.post("http://" + self._address + self._api_prefix + path, files = files_dict) \ No newline at end of file + return requests.get("http://" + self._address + self._api_prefix + path) \ No newline at end of file From bb1a616c1fa825528ce533a60f53d647f4e6129c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 11:44:20 +0200 Subject: [PATCH 029/438] Removed hardcoded url CURA-49 --- NetworkPrinterOutputDevice.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 6dcc6faf6c..0d5fb50a7c 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -151,11 +151,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._qt_part.setBody(single_string_file_data) self._qt_multi_part.append(self._qt_part) - url = "http://" + self._address + self._api_prefix + "print_job" + url = QUrl("http://" + self._address + self._api_prefix + "print_job") - url_2 = "http://10.180.0.53/api/v1/print_job" ## Create the QT request - self._qt_request = QNetworkRequest(QUrl("http://10.180.0.53/api/v1/print_job")) + self._qt_request = QNetworkRequest(url) ## Post request + data self._qt_reply = self._manager.post(self._qt_request, self._qt_multi_part) From 4090e461df300d1f5056238c929b1f9dbbf27f5b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 11:45:16 +0200 Subject: [PATCH 030/438] Setting progress won't cause devision by zero anymore CURA-49 --- NetworkPrinterOutputDevice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 0d5fb50a7c..3faa4dee04 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -173,7 +173,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.hide() def _onUploadProgress(self, bytes_sent, bytes_total): - self._progress_message.setProgress(bytes_sent / bytes_total * 100) + if bytes_total > 0: + self._progress_message.setProgress(bytes_sent / bytes_total * 100) + else: + self._progress_message.setProgress(0) def _httpGet(self, path): return requests.get("http://" + self._address + self._api_prefix + path) \ No newline at end of file From b3490ee9b99ec77ac2cba94dde1cf07e38c96100 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 12:01:53 +0200 Subject: [PATCH 031/438] Codestyle & documentation CURA-49 --- NetworkPrinterOutputDevice.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 3faa4dee04..5c834876db 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -23,13 +23,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._address = address self._key = key self._info = info - self._http_lock = threading.Lock() - self._http_connection = None - self._file = None + + self._gcode = None self._update_thread = None + # This holds the full JSON file that was received from the last request. self._json_printer_state = None + ## Todo: Hardcoded value now; we should probably read this from the machine file. + ## It's okay to leave this for now, as this plugin is um3 only (and has 2 extruders by definition) self._num_extruders = 2 self._api_version = "1" @@ -39,6 +41,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setDescription(i18n_catalog.i18nc("@info:tooltip", "Print with WIFI")) self.setIconName("print") + # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly + # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) @@ -51,6 +55,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message = None self._error_message = None + ## Get the unique key of this machine + # \return key String containing the key of the machine. def getKey(self): return self._key @@ -64,7 +70,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): try: self._spliceJSONData() except: - # issues with json parsing should not break by definition TODO: Check in what cases it should fail. + # issues with json parsing should not break by definition + # TODO: Check in what cases it should fail. pass if self._connection_state == ConnectionState.connecting: # First successful response, so we are now "connected" @@ -82,10 +89,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): except Exception as e: self.setConnectionState(ConnectionState.error) Logger.log("w", "Exception occured while connecting; %s", str(e)) - time.sleep(2) # Poll every second for printer state. + time.sleep(2) # Poll every 2 seconds for printer state. Logger.log("d", "Update thread of printer with key %s and ip %s stopped with state: %s", self._key, self._address, self._connection_state) - ## Convenience function that gets information from the recieved json data and converts it to the right internal + ## Convenience function that gets information from the received json data and converts it to the right internal # values / variables def _spliceJSONData(self): # Check for hotend temperatures @@ -103,12 +110,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): self._connection_state == ConnectionState.closed - if self._update_thread != None: + if self._update_thread is not None: self._update_thread.join() self._update_thread = None def requestWrite(self, node, file_name = None, filter_by_machine = False): - self._file = getattr(Application.getInstance().getController().getScene(), "gcode_list") + self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): @@ -135,7 +142,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.show() single_string_file_data = "" - for line in self._file: + for line in self._gcode: single_string_file_data += line ## TODO: Use correct file name (we use placeholder now) From f9a1b75c7c7585828ed0f71cd51546c1c5abacfe Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 15:00:00 +0200 Subject: [PATCH 032/438] Added timeout to get request (so we detect disconnect a lot faster) CURA-49 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 5c834876db..e33105595c 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -186,4 +186,4 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.setProgress(0) def _httpGet(self, path): - return requests.get("http://" + self._address + self._api_prefix + path) \ No newline at end of file + return requests.get("http://" + self._address + self._api_prefix + path, timeout = 2) \ No newline at end of file From e07386038999638e2051b9b3c58ceea8258ca6f8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Apr 2016 15:21:44 +0200 Subject: [PATCH 033/438] Devices are now correctly removed CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 6e674b289d..03dadf2144 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -56,7 +56,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): if self._printers[key].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._printers[key]) else: - self.getOutputDeviceManager().removeOutputDevice(self._printers[key]) + self.getOutputDeviceManager().removeOutputDevice(key) ## Handler for zeroConf detection def _onServiceChanged(self, zeroconf, service_type, name, state_change): From fd1d72380b1cd811bc691e589b61209395e03f65 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 28 Apr 2016 09:56:44 +0200 Subject: [PATCH 034/438] Increased timeout of get request CURA-49 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e33105595c..189ba9b095 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -186,4 +186,4 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.setProgress(0) def _httpGet(self, path): - return requests.get("http://" + self._address + self._api_prefix + path, timeout = 2) \ No newline at end of file + return requests.get("http://" + self._address + self._api_prefix + path, timeout = 5) \ No newline at end of file From 2345289a4e64d0d25859e5e8e1a0e76b94144123 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 12 May 2016 16:43:35 +0200 Subject: [PATCH 035/438] Fix an error when NetworkPrinterOutputDevicePlugin.addPrinter is called before there is an Active Machine Instance CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 03dadf2144..ec367d96d9 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -47,7 +47,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def addPrinter(self, name, address, properties): printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) self._printers[printer.getKey()] = printer - if printer.getKey() == Application.getInstance().getMachineManager().getActiveMachineInstance().getKey(): + active_machine_instance = Application.getInstance().getMachineManager().getActiveMachineInstance() + if active_machine_instance and printer.getKey() == active_machine_instance.getKey(): self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) From 3f4e740c0dfec9b9adc3aaf3969db43eaead8f7c Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Wed, 18 May 2016 17:58:15 +0200 Subject: [PATCH 036/438] Add a CMakeLists file so the plugin can be installed --- CMakeLists.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000..906af9910c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +project(JediWifiPrintingPlugin) +cmake_minimum_required(VERSION 2.8.12) + +install(FILES + __init__.py + HttpUploadDataStream.py + NetworkPrinterOutputDevice.py + NetworkPrinterOutputDevicePlugin.py + LICENSE + README.md + DESTINATION lib/cura/plugins/JediWifiPrintingPlugin +) From 65f329623f65e9e8f931494f9bb1cfdb9b40d739 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Mon, 30 May 2016 15:37:33 +0200 Subject: [PATCH 037/438] Update plugin to API version 3 Contributes to CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 13 +++++++------ __init__.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index ec367d96d9..9e8a43b187 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -18,7 +18,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addPrinterSignal.connect(self.addPrinter) - Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineInstanceChanged) + Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) addPrinterSignal = Signal() @@ -30,12 +30,13 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def stop(self): self._zero_conf.close() - def _onActiveMachineInstanceChanged(self): + def _onGlobalContainerStackChanged(self): try: - active_machine_key = Application.getInstance().getMachineManager().getActiveMachineInstance().getKey() + active_machine_key = Application.getInstance().getGlobalContainerStack().getId() except AttributeError: ## Active machine instance changed to None. This can happen upon clean start. Simply ignore. return + for key in self._printers: if key == active_machine_key: self._printers[key].connect() @@ -47,8 +48,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def addPrinter(self, name, address, properties): printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) self._printers[printer.getKey()] = printer - active_machine_instance = Application.getInstance().getMachineManager().getActiveMachineInstance() - if active_machine_instance and printer.getKey() == active_machine_instance.getKey(): + stack = Application.getInstance().getGlobalContainerStack() + if stack and printer.getKey() == stack.getKey(): self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) @@ -71,4 +72,4 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): elif state_change == ServiceStateChange.Removed: pass # TODO; This isn't testable right now. We need to also decide how to handle - # \ No newline at end of file + # diff --git a/__init__.py b/__init__.py index b5aaa03513..76d4094709 100644 --- a/__init__.py +++ b/__init__.py @@ -11,7 +11,7 @@ def getMetaData(): "name": "Wifi connection", "author": "Ultimaker", "description": catalog.i18nc("Wifi connection", "Wifi connection"), - "api": 2 + "api": 3 } } From 025bdba516a1eef94d663c76eeb94a519cc80017 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 13 Jun 2016 13:34:22 +0200 Subject: [PATCH 038/438] Changes to make the network plugin play well with setting_rework CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 18 ++++++++---------- __init__.py | 3 +-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 9e8a43b187..fe8629f8d6 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -18,7 +18,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addPrinterSignal.connect(self.addPrinter) - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) + Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) addPrinterSignal = Signal() @@ -30,15 +30,14 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def stop(self): self._zero_conf.close() - def _onGlobalContainerStackChanged(self): - try: - active_machine_key = Application.getInstance().getGlobalContainerStack().getId() - except AttributeError: - ## Active machine instance changed to None. This can happen upon clean start. Simply ignore. + def _onGlobalStackChanged(self): + + active_machine = Application.getInstance().getGlobalContainerStack() + if not active_machine: return for key in self._printers: - if key == active_machine_key: + if key == active_machine.getKey(): self._printers[key].connect() self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) else: @@ -48,8 +47,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): def addPrinter(self, name, address, properties): printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) self._printers[printer.getKey()] = printer - stack = Application.getInstance().getGlobalContainerStack() - if stack and printer.getKey() == stack.getKey(): + if printer.getKey() == Application.getInstance().getGlobalContainerStack().getKey(): self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) @@ -72,4 +70,4 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): elif state_change == ServiceStateChange.Removed: pass # TODO; This isn't testable right now. We need to also decide how to handle - # + # \ No newline at end of file diff --git a/__init__.py b/__init__.py index 76d4094709..d91262f7ba 100644 --- a/__init__.py +++ b/__init__.py @@ -16,5 +16,4 @@ def getMetaData(): } def register(app): - return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin()} - + return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin()} \ No newline at end of file From bd413b9a2b1e4358e0f766c82161c50b0c77a42d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 14 Jun 2016 09:42:53 +0200 Subject: [PATCH 039/438] Added signalemitter CURA-49 --- NetworkPrinterOutputDevice.py | 2 ++ NetworkPrinterOutputDevicePlugin.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 189ba9b095..551f575686 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -5,6 +5,7 @@ import requests from UM.i18n import i18nCatalog from UM.Application import Application from UM.Logger import Logger +from UM.Signal import signalemitter from UM.Message import Message @@ -17,6 +18,7 @@ i18n_catalog = i18nCatalog("cura") ## Network connected (wifi / lan) printer that uses the Ultimaker API +@signalemitter class NetworkPrinterOutputDevice(PrinterOutputDevice): def __init__(self, key, address, info): super().__init__(key) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index fe8629f8d6..946e510608 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -2,14 +2,15 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from . import NetworkPrinterOutputDevice from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange -from UM.Signal import Signal, SignalEmitter +from UM.Signal import Signal, signalemitter from UM.Application import Application ## This plugin handles the connection detection & creation of output device objects for the UM3 printer. # Zero-Conf is used to detect printers, which are saved in a dict. # If we discover a printer that has the same key as the active machine instance a connection is made. -class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): +@signalemitter +class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def __init__(self): super().__init__() self._zero_conf = Zeroconf() @@ -37,17 +38,16 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin, SignalEmitter): return for key in self._printers: - if key == active_machine.getKey(): + if key == active_machine.getMetaDataEntry("key"): self._printers[key].connect() self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) - else: self._printers[key].close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addPrinter(self, name, address, properties): printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) self._printers[printer.getKey()] = printer - if printer.getKey() == Application.getInstance().getGlobalContainerStack().getKey(): + if printer.getKey() == Application.getInstance().getGlobalContainerStack().getMetaDataEntry("key"): self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) From 486fc7f17643332313689a5f222f93deb069a043 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 14 Jun 2016 09:46:01 +0200 Subject: [PATCH 040/438] When no globalcontainer is set, no check to connect is performed CURA-49 --- NetworkPrinterOutputDevicePlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 946e510608..9c187e5065 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -47,9 +47,10 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def addPrinter(self, name, address, properties): printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) self._printers[printer.getKey()] = printer - if printer.getKey() == Application.getInstance().getGlobalContainerStack().getMetaDataEntry("key"): + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("key"): self._printers[printer.getKey()].connect() - printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) ## Handler for when the connection state of one of the detected printers changes def _onPrinterConnectionStateChanged(self, key): From 1acf155e19a16afbf4f5d5f7da7ac6e83002820b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 14 Jun 2016 11:49:28 +0200 Subject: [PATCH 041/438] We don't depend on requests lib anymore Intead we use qt network stuff for everything. CURA-49 --- NetworkPrinterOutputDevice.py | 126 +++++++++++++++++----------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 551f575686..b3ae5bb40b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -1,7 +1,3 @@ -import threading -import time -import requests - from UM.i18n import i18nCatalog from UM.Application import Application from UM.Logger import Logger @@ -12,7 +8,9 @@ from UM.Message import Message from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import QUrl, QTimer + +import json i18n_catalog = i18nCatalog("cura") @@ -27,7 +25,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._info = info self._gcode = None - self._update_thread = None # This holds the full JSON file that was received from the last request. self._json_printer_state = None @@ -49,50 +46,40 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._manager.finished.connect(self._onFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) - self._qt_request = None - self._qt_reply = None - self._qt_multi_part = None - self._qt_part = None + self._printer_request = None + self._printer_reply = None + + self._print_job_request = None + self._print_job_reply = None + + self._post_request = None + self._post_reply = None + self._post_multi_part = None + self._post_part = None self._progress_message = None self._error_message = None + self._update_timer = QTimer() + self._update_timer.setInterval(5000) # TODO; Add preference for update interval + self._update_timer.setSingleShot(False) + self._update_timer.timeout.connect(self._update) + ## Get the unique key of this machine # \return key String containing the key of the machine. def getKey(self): return self._key def _update(self): - Logger.log("d", "Update thread of printer with key %s and ip %s started", self._key, self._address) - while self.isConnected(): - try: - printer_reply = self._httpGet("printer") - if printer_reply.status_code == 200: - self._json_printer_state = printer_reply.json() - try: - self._spliceJSONData() - except: - # issues with json parsing should not break by definition - # TODO: Check in what cases it should fail. - pass - if self._connection_state == ConnectionState.connecting: - # First successful response, so we are now "connected" - self.setConnectionState(ConnectionState.connected) - else: - Logger.log("w", "Got http status code %s while trying to request data.", printer_reply.status_code) - self.setConnectionState(ConnectionState.error) + ## Request 'general' printer data + url = QUrl("http://" + self._address + self._api_prefix + "printer") + self._printer_request = QNetworkRequest(url) + self._printer_reply = self._manager.get(self._printer_request) - print_job_reply = self._httpGet("print_job") - if print_job_reply.status_code != 404: - self.setProgress(print_job_reply.json()["progress"]) - else: - self.setProgress(0) - - except Exception as e: - self.setConnectionState(ConnectionState.error) - Logger.log("w", "Exception occured while connecting; %s", str(e)) - time.sleep(2) # Poll every 2 seconds for printer state. - Logger.log("d", "Update thread of printer with key %s and ip %s stopped with state: %s", self._key, self._address, self._connection_state) + ## Request print_job data + url = QUrl("http://" + self._address + self._api_prefix + "print_job") + self._print_job_request = QNetworkRequest(url) + self._print_job_reply = self._manager.get(self._print_job_request) ## Convenience function that gets information from the received json data and converts it to the right internal # values / variables @@ -112,9 +99,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): self._connection_state == ConnectionState.closed - if self._update_thread is not None: - self._update_thread.join() - self._update_thread = None + self._update_timer.stop() def requestWrite(self, node, file_name = None, filter_by_machine = False): self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") @@ -123,13 +108,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error - ## Start the polling thread. + ## Start requesting data from printer def connect(self): - if self._update_thread is None: - self.setConnectionState(ConnectionState.connecting) - self._update_thread = threading.Thread(target = self._update) - self._update_thread.daemon = True - self._update_thread.start() + self.setConnectionState(ConnectionState.connecting) + self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. + Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) + self._update_timer.start() def getCameraImage(self): pass # TODO: This still needs to be implemented (we don't have a place to show it now anyway) @@ -143,6 +127,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() + ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line @@ -151,23 +136,23 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): file_name = "test.gcode" ## Create multi_part request - self._qt_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) + self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create part (to be placed inside multipart) - self._qt_part = QHttpPart() - self._qt_part.setHeader(QNetworkRequest.ContentDispositionHeader, + self._post_part = QHttpPart() + self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) - self._qt_part.setBody(single_string_file_data) - self._qt_multi_part.append(self._qt_part) + self._post_part.setBody(single_string_file_data.encode()) + self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "print_job") ## Create the QT request - self._qt_request = QNetworkRequest(url) + self._post_request = QNetworkRequest(url) ## Post request + data - self._qt_reply = self._manager.post(self._qt_request, self._qt_multi_part) - self._qt_reply.uploadProgress.connect(self._onUploadProgress) + self._post_reply = self._manager.post(self._post_request, self._post_multi_part) + self._post_reply.uploadProgress.connect(self._onUploadProgress) except IOError: self._progress_message.hide() @@ -177,15 +162,32 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.hide() Logger.log("e" , "An exception occured in network connection: %s" % str(e)) + ## Handler for all requests that have finshed. def _onFinished(self, reply): - reply.uploadProgress.disconnect(self._onUploadProgress) - self._progress_message.hide() + if reply.operation() == QNetworkAccessManager.GetOperation: + if "printer" in reply.url().toString(): # Status update from printer. + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + if self._connection_state == ConnectionState.connecting: + self.setConnectionState(ConnectionState.connected) + self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8")) + + self._spliceJSONData() + else: + pass # TODO: Handle errors + elif "print_job" in reply.url().toString(): # Status update from print_job: + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + self.setProgress(json.loads(bytes(reply.readAll()).decode("utf-8"))["progress"]) + elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: + self.setProgress(0) # No print job found, so there can't be progress! + + elif reply.operation() == QNetworkAccessManager.PostOperation: + reply.uploadProgress.disconnect(self._onUploadProgress) + self._progress_message.hide() + else: + print("got unhandled operation:", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: - self._progress_message.setProgress(0) - - def _httpGet(self, path): - return requests.get("http://" + self._address + self._api_prefix + path, timeout = 5) \ No newline at end of file + self._progress_message.setProgress(0) \ No newline at end of file From 1220d32ca56c5452ce6a8adb8e4da4c9fd1b486a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 14 Jun 2016 13:23:05 +0200 Subject: [PATCH 042/438] No longer possible to send print_jobs when print just started CURA-49 --- NetworkPrinterOutputDevice.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index b3ae5bb40b..6fddf2d284 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -160,9 +160,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message.show() except Exception as e: self._progress_message.hide() - Logger.log("e" , "An exception occured in network connection: %s" % str(e)) + Logger.log("e", "An exception occurred in network connection: %s" % str(e)) - ## Handler for all requests that have finshed. + ## Handler for all requests that have finished. def _onFinished(self, reply): if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. @@ -176,7 +176,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass # TODO: Handle errors elif "print_job" in reply.url().toString(): # Status update from print_job: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: - self.setProgress(json.loads(bytes(reply.readAll()).decode("utf-8"))["progress"]) + progress = json.loads(bytes(reply.readAll()).decode("utf-8"))["progress"] + ## If progress is 0 add a bit so another print can't be sent. + if progress == 0: + progress += 0.1 + self.setProgress(progress) elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! @@ -184,7 +188,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() else: - print("got unhandled operation:", reply.operation()) + Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: From cadf5d85b79c8a85b81aff11bb762069835436c1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 14 Jun 2016 13:46:04 +0200 Subject: [PATCH 043/438] Camera image is now also retrieved CURA-338 --- NetworkPrinterOutputDevice.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 6fddf2d284..c897df8a60 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -9,6 +9,7 @@ from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager from PyQt5.QtCore import QUrl, QTimer +from PyQt5.QtGui import QPixmap import json @@ -52,6 +53,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._print_job_request = None self._print_job_reply = None + self._image_request = None + self._image_reply = None + self._post_request = None self._post_reply = None self._post_multi_part = None @@ -65,6 +69,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) + self._camera_image = QPixmap() + ## Get the unique key of this machine # \return key String containing the key of the machine. def getKey(self): @@ -81,6 +87,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._print_job_request = QNetworkRequest(url) self._print_job_reply = self._manager.get(self._print_job_request) + ## Request new image + url = QUrl("http://" + self._address +":8080/?action=snapshot") + self._image_request = QNetworkRequest(url) + self._image_reply = self._manager.get(self._image_request) + ## Convenience function that gets information from the received json data and converts it to the right internal # values / variables def _spliceJSONData(self): @@ -116,7 +127,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.start() def getCameraImage(self): - pass # TODO: This still needs to be implemented (we don't have a place to show it now anyway) + return self._camera_image def startPrint(self): if self._progress != 0: @@ -183,7 +194,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setProgress(progress) elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! - + elif "snapshot" in reply.url().toString(): # Status update from image: + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + self._camera_image.loadFromData(reply.readAll()) elif reply.operation() == QNetworkAccessManager.PostOperation: reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() From ac4dbe2304194bcfa4250443030f85ffd6fa20ad Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 14 Jun 2016 16:01:06 +0200 Subject: [PATCH 044/438] Decreased the progress added when 0 was returned Turned out that by adding 0.1, it started at 10% --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index c897df8a60..712c48a865 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -190,7 +190,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): progress = json.loads(bytes(reply.readAll()).decode("utf-8"))["progress"] ## If progress is 0 add a bit so another print can't be sent. if progress == 0: - progress += 0.1 + progress += 0.001 self.setProgress(progress) elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! From 7d258485c8223a7e35b2d1ae6cf8f5150432a007 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 15 Jun 2016 14:17:59 +0200 Subject: [PATCH 045/438] Small fixes so switching printers correctly connects / disconnects output devices CURA-1036 --- NetworkPrinterOutputDevice.py | 2 +- NetworkPrinterOutputDevicePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 712c48a865..efc34ecf52 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -109,7 +109,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._updateHeadPosition(head_x, head_y, head_z) def close(self): - self._connection_state == ConnectionState.closed + self.setConnectionState(ConnectionState.closed) self._update_timer.stop() def requestWrite(self, node, file_name = None, filter_by_machine = False): diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 9c187e5065..fed761b772 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -32,7 +32,6 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._zero_conf.close() def _onGlobalStackChanged(self): - active_machine = Application.getInstance().getGlobalContainerStack() if not active_machine: return @@ -41,6 +40,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if key == active_machine.getMetaDataEntry("key"): self._printers[key].connect() self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + else: self._printers[key].close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. From 61565561ffe9341d497cb71971fb53919f64c3cb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 15 Jun 2016 15:05:28 +0200 Subject: [PATCH 046/438] Temperatures for both extruders are now correctly retrieved CURA-1036 --- NetworkPrinterOutputDevice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index efc34ecf52..33e3da64de 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -34,6 +34,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## It's okay to leave this for now, as this plugin is um3 only (and has 2 extruders by definition) self._num_extruders = 2 + self._hotend_temperatures = [0] * self._num_extruders + self._target_hotend_temperatures = [0] * self._num_extruders + self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" self.setName(key) @@ -96,7 +99,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # values / variables def _spliceJSONData(self): # Check for hotend temperatures - for index in range(0, self._num_extruders - 1): + for index in range(0, self._num_extruders): temperature = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["temperature"]["current"] self._setHotendTemperature(index, temperature) From 99a30573ce8bb5e734b8409201435a8a3f0d8dd4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 15 Jun 2016 16:28:05 +0200 Subject: [PATCH 047/438] Store current temp in bed_temperature instead of dict with target & current CURA-1036 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 33e3da64de..57a7086b97 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -103,7 +103,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): temperature = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["temperature"]["current"] self._setHotendTemperature(index, temperature) - bed_temperature = self._json_printer_state["bed"]["temperature"] + bed_temperature = self._json_printer_state["bed"]["temperature"]["current"] self._setBedTemperature(bed_temperature) head_x = self._json_printer_state["heads"][0]["position"]["x"] From df943736645c579df0919ae3f388246cd393e749 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 15 Jun 2016 16:46:41 +0200 Subject: [PATCH 048/438] TimeTotal and timeSpent are now set CURA-1068 --- NetworkPrinterOutputDevice.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 57a7086b97..f943e97bc3 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -190,11 +190,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass # TODO: Handle errors elif "print_job" in reply.url().toString(): # Status update from print_job: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: - progress = json.loads(bytes(reply.readAll()).decode("utf-8"))["progress"] + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + progress = json_data["progress"] ## If progress is 0 add a bit so another print can't be sent. if progress == 0: progress += 0.001 self.setProgress(progress) + + self.setTimeElapsed(json_data["time_elapsed"]) + self.setTimeTotal(json_data["time_total"]) + elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! elif "snapshot" in reply.url().toString(): # Status update from image: From 354d2bc10984a5da67c9332146120bcfb1eca284 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Jun 2016 09:36:19 +0200 Subject: [PATCH 049/438] Camera image now works with a image provider CURA-1036 and CURA-338 --- NetworkPrinterOutputDevice.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f943e97bc3..c384d0f710 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -8,8 +8,8 @@ from UM.Message import Message from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager -from PyQt5.QtCore import QUrl, QTimer -from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty +from PyQt5.QtGui import QPixmap, QImage import json @@ -72,7 +72,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) - self._camera_image = QPixmap() + self._camera_image_id = 0 + + self._camera_image = QImage() ## Get the unique key of this machine # \return key String containing the key of the machine. @@ -129,6 +131,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) self._update_timer.start() + newImage = pyqtSignal() + + @pyqtProperty(QUrl, notify = newImage) + def cameraImage(self): + self._camera_image_id += 1 + temp = "image://camera/" + str(self._camera_image_id) + return QUrl(temp, QUrl.TolerantMode) + def getCameraImage(self): return self._camera_image @@ -205,6 +215,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif "snapshot" in reply.url().toString(): # Status update from image: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) + self.newImage.emit() elif reply.operation() == QNetworkAccessManager.PostOperation: reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() From bb1fead4c66345f83c7635c2320cb5aebf96a9f3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Jun 2016 09:40:48 +0200 Subject: [PATCH 050/438] Moved camera to own timer, so grabbing can be in a different frequency CURA-336 and CURA-1036 --- NetworkPrinterOutputDevice.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index c384d0f710..f979db2f6d 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -72,6 +72,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) + self._camera_timer = QTimer() + self._camera_timer.setInterval(2000) # Todo: Add preference for camera update interval + self._camera_timer.setSingleShot(False) + self._camera_timer.timeout.connect(self._update_camera) + self._camera_image_id = 0 self._camera_image = QImage() @@ -81,6 +86,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def getKey(self): return self._key + def _update_camera(self): + ## Request new image + url = QUrl("http://" + self._address + ":8080/?action=snapshot") + self._image_request = QNetworkRequest(url) + self._image_reply = self._manager.get(self._image_request) + def _update(self): ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") @@ -92,11 +103,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._print_job_request = QNetworkRequest(url) self._print_job_reply = self._manager.get(self._print_job_request) - ## Request new image - url = QUrl("http://" + self._address +":8080/?action=snapshot") - self._image_request = QNetworkRequest(url) - self._image_reply = self._manager.get(self._image_request) - ## Convenience function that gets information from the received json data and converts it to the right internal # values / variables def _spliceJSONData(self): @@ -116,6 +122,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): self.setConnectionState(ConnectionState.closed) self._update_timer.stop() + self._camera_timer.stop() def requestWrite(self, node, file_name = None, filter_by_machine = False): self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") @@ -128,8 +135,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def connect(self): self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. + self._update_camera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) self._update_timer.start() + self._camera_timer.start() newImage = pyqtSignal() From c6b88118dcd467d697e0271df6752163b51c7d16 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Jun 2016 11:43:53 +0200 Subject: [PATCH 051/438] Jobstate is now also tracked --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f979db2f6d..2a9a71b3ac 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -215,7 +215,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if progress == 0: progress += 0.001 self.setProgress(progress) - + self.setJobState(json_data["state"]) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) From 9dbdd7fe7a311d513f76c4206efe03f9cec03f5d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 16 Jun 2016 15:35:40 +0200 Subject: [PATCH 052/438] Added changing printjob state This also adds the bits required to actually do a put request CURA-1036 --- NetworkPrinterOutputDevice.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2a9a71b3ac..cae59fd287 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -64,6 +64,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_multi_part = None self._post_part = None + self._put_request = None + self._put_reply = None + self._progress_message = None self._error_message = None @@ -151,6 +154,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def getCameraImage(self): return self._camera_image + def _setJobState(self, job_state): + url = QUrl("http://" + self._address + self._api_prefix + "print_job/state") + self._put_request = QNetworkRequest(url) + self._put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + data = "{\"target\": \"%s\"}" % job_state + self._put_reply = self._manager.put(self._put_request, data.encode()) + def startPrint(self): if self._progress != 0: self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) @@ -215,7 +225,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if progress == 0: progress += 0.001 self.setProgress(progress) - self.setJobState(json_data["state"]) + self._updateJobState(json_data["state"]) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) @@ -228,6 +238,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif reply.operation() == QNetworkAccessManager.PostOperation: reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() + elif reply.operation() == QNetworkAccessManager.PutOperation: + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 204: + pass # Request was sucesfull! + else: + Logger.log("d","Something went wrong when trying to update data of API. %s", reply.readAll()) else: Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) From a36157c538d619bad39ed07dfc0650fd35feeadb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Jun 2016 11:18:10 +0200 Subject: [PATCH 053/438] Jobstate is reset when there is no print job CURA-1036 --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index cae59fd287..bcc8283771 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -231,6 +231,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! + self._updateJobState("") elif "snapshot" in reply.url().toString(): # Status update from image: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) From 44c2a091182ed8397fcff4ccee748e5aa5d3459c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Jun 2016 11:34:28 +0200 Subject: [PATCH 054/438] Added stubs for checking if a print is at all possible CURA-1036 --- NetworkPrinterOutputDevice.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index bcc8283771..fc6a75dde0 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -129,6 +129,19 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def requestWrite(self, node, file_name = None, filter_by_machine = False): self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") + + # TODO: Implement all checks. + # Check if cartridges are loaded at all (Error) + #self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"] != "" + + # Check if there is material loaded at all (Error) + #self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"] != "" + + # Check if there is enough material (Warning) + #self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["length_remaining"] + + #TODO: Check if the cartridge is the right ID (give warning otherwise) + self.startPrint() def isConnected(self): From 0b9af6055cdf0fcf6f9c0fb9c0e364c3524f1cf9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Jun 2016 11:34:54 +0200 Subject: [PATCH 055/438] Decreased sample time for print information CURA-1036 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index fc6a75dde0..7802767647 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -71,7 +71,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message = None self._update_timer = QTimer() - self._update_timer.setInterval(5000) # TODO; Add preference for update interval + self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) From be779cb92aa8e31dfff8d1db30f3c3a5b6ce386c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Jun 2016 13:00:05 +0200 Subject: [PATCH 056/438] Improved logging when put operation failed CURA-1036 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 7802767647..e9c8c483bd 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -256,7 +256,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 204: pass # Request was sucesfull! else: - Logger.log("d","Something went wrong when trying to update data of API. %s", reply.readAll()) + Logger.log("d","Something went wrong when trying to update data of API. %s statuscode: %s", reply.readAll(), reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) else: Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) From f8cac563705f383ccd033b99711f6d7eb0186c01 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Jun 2016 15:46:18 +0200 Subject: [PATCH 057/438] Added DiscoverUM3 machine action stub CURA-1385 --- DiscoverUM3Action.py | 5 +++++ __init__.py | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 DiscoverUM3Action.py diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py new file mode 100644 index 0000000000..c5c853cca8 --- /dev/null +++ b/DiscoverUM3Action.py @@ -0,0 +1,5 @@ +from cura.MachineAction import MachineAction + +class DiscoverUM3Action(MachineAction): + def __init__(self): + super().__init__("DiscoverUM3Action") \ No newline at end of file diff --git a/__init__.py b/__init__.py index d91262f7ba..1efc63e8d0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. from . import NetworkPrinterOutputDevicePlugin - +from . import DiscoverUM3Action from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -12,8 +12,11 @@ def getMetaData(): "author": "Ultimaker", "description": catalog.i18nc("Wifi connection", "Wifi connection"), "api": 3 - } + }, + "profile_reader": [ + {} + ] } def register(app): - return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin()} \ No newline at end of file + return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()} \ No newline at end of file From bca613b47e7b44e04d61eacca2d0e45a09636f11 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Jun 2016 13:43:43 +0200 Subject: [PATCH 058/438] Added label CURA-1385 --- DiscoverUM3Action.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index c5c853cca8..848e1c26f2 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -2,4 +2,7 @@ from cura.MachineAction import MachineAction class DiscoverUM3Action(MachineAction): def __init__(self): - super().__init__("DiscoverUM3Action") \ No newline at end of file + super().__init__("DiscoverUM3Action", "Discover printers") + + def _execute(self): + pass \ No newline at end of file From d7bb807e2b0ccb927932fe999e1913378b4df3d8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 13:35:25 +0200 Subject: [PATCH 059/438] Added stub QML page for discovery action CURA-336 --- DiscoverUM3Action.py | 1 + DiscoverUM3Action.qml | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 DiscoverUM3Action.qml diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 848e1c26f2..89b024b8bd 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -3,6 +3,7 @@ from cura.MachineAction import MachineAction class DiscoverUM3Action(MachineAction): def __init__(self): super().__init__("DiscoverUM3Action", "Discover printers") + self._qml_url = "DiscoverUM3Action.qml" def _execute(self): pass \ No newline at end of file diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml new file mode 100644 index 0000000000..1d26d143f8 --- /dev/null +++ b/DiscoverUM3Action.qml @@ -0,0 +1,26 @@ +import UM 1.2 as UM +import Cura 1.0 as Cura + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 + +Cura.MachineAction +{ + anchors.fill: parent; + Item + { + anchors.fill: parent; + id: discoverUM3Action + UM.I18nCatalog { id: catalog; name:"cura"} + Label + { + id: pageTitle + width: parent.width + text: catalog.i18nc("@title", "Discover Printer") + wrapMode: Text.WordWrap + font.pointSize: 18; + } + } +} \ No newline at end of file From f199d490bb7de893fc68d2064faad2f853680361 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 14:04:50 +0200 Subject: [PATCH 060/438] Action now has a list of found UM3 keys CURA-336 --- DiscoverUM3Action.py | 27 +++++++++++++++++++++++++-- DiscoverUM3Action.qml | 5 +++++ NetworkPrinterOutputDevicePlugin.py | 3 +++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 89b024b8bd..48cd3de247 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -1,9 +1,32 @@ from cura.MachineAction import MachineAction +from UM.Application import Application + +from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot + class DiscoverUM3Action(MachineAction): def __init__(self): super().__init__("DiscoverUM3Action", "Discover printers") self._qml_url = "DiscoverUM3Action.qml" - def _execute(self): - pass \ No newline at end of file + self._network_plugin = None + + printerDetected = pyqtSignal() + + @pyqtSlot() + def startDiscovery(self): + if not self._network_plugin: + self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("JediWifiPrintingPlugin") + self._network_plugin.addPrinterSignal.connect(self._onPrinterAdded) + self.printerDetected.emit() + + def _onPrinterAdded(self, *args): + self.printerDetected.emit() + + @pyqtProperty("QVariantList", notify = printerDetected) + def foundDevices(self): + if self._network_plugin: + print(list(self._network_plugin.getPrinters().keys())) + return list(self._network_plugin.getPrinters().keys()) + else: + return [] diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 1d26d143f8..7935298008 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -22,5 +22,10 @@ Cura.MachineAction wrapMode: Text.WordWrap font.pointSize: 18; } + Button + { + text: "Start looking!" + onClicked: manager.startDiscovery() + } } } \ No newline at end of file diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index fed761b772..cd4de6e4f8 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -31,6 +31,9 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def stop(self): self._zero_conf.close() + def getPrinters(self): + return self._printers + def _onGlobalStackChanged(self): active_machine = Application.getInstance().getGlobalContainerStack() if not active_machine: From 9a4fc0345c6a55fab6bccbd807ec2c72585863bb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 14:17:50 +0200 Subject: [PATCH 061/438] All found devices are now listed in the discover action CURA-336 --- DiscoverUM3Action.py | 2 +- DiscoverUM3Action.qml | 23 ++++++++++++++++++++++- NetworkPrinterOutputDevicePlugin.py | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 48cd3de247..be71a05e28 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -26,7 +26,7 @@ class DiscoverUM3Action(MachineAction): @pyqtProperty("QVariantList", notify = printerDetected) def foundDevices(self): if self._network_plugin: - print(list(self._network_plugin.getPrinters().keys())) + return list(self._network_plugin.getPrinters().keys()) else: return [] diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 7935298008..b9883cb47c 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -9,7 +9,7 @@ import QtQuick.Window 2.1 Cura.MachineAction { anchors.fill: parent; - Item + Column { anchors.fill: parent; id: discoverUM3Action @@ -27,5 +27,26 @@ Cura.MachineAction text: "Start looking!" onClicked: manager.startDiscovery() } + + ListView + { + model: manager.foundDevices + width: parent.width + height: 500 + delegate: Rectangle + { + height: childrenRect.height; + color: "white" + width: parent.width + Label + { + anchors.left: parent.left; + anchors.leftMargin: UM.Theme.getSize("default_margin").width; + anchors.right: parent.right; + text: modelData + elide: Text.ElideRight + } + } + } } } \ No newline at end of file diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index cd4de6e4f8..6697e05bd9 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -67,7 +67,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if state_change == ServiceStateChange.Added: info = zeroconf.get_service_info(service_type, name) if info: - if info.properties.get(b"type", None): + if info.properties.get(b"type", None) == b'printer': address = '.'.join(map(lambda n: str(n), info.address)) self.addPrinterSignal.emit(str(name), address, info.properties) From 16fcf2208b1edb11d5f6d6fcfff2cec6f1024bd1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 14:19:56 +0200 Subject: [PATCH 062/438] Refactoring & exposing of properties CURA-336 --- NetworkPrinterOutputDevice.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e9c8c483bd..02646bbba2 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -19,11 +19,11 @@ i18n_catalog = i18nCatalog("cura") ## Network connected (wifi / lan) printer that uses the Ultimaker API @signalemitter class NetworkPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, key, address, info): + def __init__(self, key, address, properties): super().__init__(key) self._address = address self._key = key - self._info = info + self._properties = properties # Properties dict as provided by zero conf self._gcode = None @@ -41,7 +41,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._api_prefix = "/api/v" + self._api_version + "/" self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with WIFI")) - self.setDescription(i18n_catalog.i18nc("@info:tooltip", "Print with WIFI")) + self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with WIFI")) self.setIconName("print") # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly @@ -84,6 +84,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_image = QImage() + def getProperties(self): + return self._properties + ## Get the unique key of this machine # \return key String containing the key of the machine. def getKey(self): From 0cee5b78df96f44d7dae7b632fc003bd8ce17782 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 14:25:46 +0200 Subject: [PATCH 063/438] Print discovery now lists all found human readable names CURA-336 --- DiscoverUM3Action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index be71a05e28..68da635e11 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -26,7 +26,7 @@ class DiscoverUM3Action(MachineAction): @pyqtProperty("QVariantList", notify = printerDetected) def foundDevices(self): if self._network_plugin: - - return list(self._network_plugin.getPrinters().keys()) + printers = self._network_plugin.getPrinters() + return([printers[printer].getProperties().get(b"name").decode("utf-8") for printer in printers]) else: return [] From e94f94767e95df0a9de528287107e77177914250 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 14:39:50 +0200 Subject: [PATCH 064/438] Updated UX of detected printers CURA-336 --- DiscoverUM3Action.py | 2 +- DiscoverUM3Action.qml | 70 +++++++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 68da635e11..c8dd2eda41 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -27,6 +27,6 @@ class DiscoverUM3Action(MachineAction): def foundDevices(self): if self._network_plugin: printers = self._network_plugin.getPrinters() - return([printers[printer].getProperties().get(b"name").decode("utf-8") for printer in printers]) + return [printers[printer].getProperties().get(b"name").decode("utf-8") for printer in printers] else: return [] diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index b9883cb47c..100de777c7 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -13,38 +13,78 @@ Cura.MachineAction { anchors.fill: parent; id: discoverUM3Action + SystemPalette { id: palette } UM.I18nCatalog { id: catalog; name:"cura"} Label { id: pageTitle width: parent.width - text: catalog.i18nc("@title", "Discover Printer") + text: catalog.i18nc("@title", "Connect to Networked Printer") wrapMode: Text.WordWrap font.pointSize: 18; } + + Label + { + id: pageDescription + width: parent.width + wrapMode: Text.WordWrap + text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your ptiner is connected to the network using a network cable of by connecting your printer to your WIFI network. \n\n If you don't want to connect Cura with your Ultimaker 3 now, you can always use a USB drive to transfer g-code files to your Printer.\n\n Select your Ultimaker 3 from the list below:") + } Button { text: "Start looking!" onClicked: manager.startDiscovery() } - - ListView + Row { - model: manager.foundDevices width: parent.width - height: 500 - delegate: Rectangle + ScrollView { - height: childrenRect.height; - color: "white" - width: parent.width - Label + id: objectListContainer + frameVisible: true; + width: parent.width * 0.5 + + Rectangle { - anchors.left: parent.left; - anchors.leftMargin: UM.Theme.getSize("default_margin").width; - anchors.right: parent.right; - text: modelData - elide: Text.ElideRight + parent: viewport + anchors.fill: parent + color: palette.light + } + + ListView + { + model: manager.foundDevices + width: parent.width + height: 500 + currentIndex: activeIndex + delegate: Rectangle + { + height: childrenRect.height; + color: ListView.isCurrentItem ? palette.highlight : index % 2 ? palette.base : palette.alternateBase + width: parent.width + Label + { + anchors.left: parent.left; + anchors.leftMargin: UM.Theme.getSize("default_margin").width; + anchors.right: parent.right; + text: modelData + elide: Text.ElideRight + } + + MouseArea + { + anchors.fill: parent; + onClicked: + { + if(!parent.ListView.isCurrentItem) + { + parent.ListView.view.currentIndex = index; + //base.itemActivated(); + } + } + } + } } } } From 01eb65f30360e5d8ebb1acb34da9db2b2bddb971 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 15:02:45 +0200 Subject: [PATCH 065/438] Removed accidental profile_reader stuff --- __init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 1efc63e8d0..711b7caf93 100644 --- a/__init__.py +++ b/__init__.py @@ -12,10 +12,7 @@ def getMetaData(): "author": "Ultimaker", "description": catalog.i18nc("Wifi connection", "Wifi connection"), "api": 3 - }, - "profile_reader": [ - {} - ] + } } def register(app): From 3b51c31772cdfc7c8c0b34a8064edef5af938f5e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 15:46:12 +0200 Subject: [PATCH 066/438] Meta data of the printer is now also visible CURA-336 --- DiscoverUM3Action.py | 2 +- DiscoverUM3Action.qml | 60 +++++++++++++++++++++++++++++++++-- NetworkPrinterOutputDevice.py | 15 +++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index c8dd2eda41..aade48e806 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -27,6 +27,6 @@ class DiscoverUM3Action(MachineAction): def foundDevices(self): if self._network_plugin: printers = self._network_plugin.getPrinters() - return [printers[printer].getProperties().get(b"name").decode("utf-8") for printer in printers] + return [printers[printer] for printer in printers] else: return [] diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 100de777c7..fcb529536e 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -8,7 +8,9 @@ import QtQuick.Window 2.1 Cura.MachineAction { + id: base anchors.fill: parent; + property var selectedPrinter: null Column { anchors.fill: parent; @@ -39,6 +41,7 @@ Cura.MachineAction Row { width: parent.width + spacing: UM.Theme.getSize("default_margin").width ScrollView { id: objectListContainer @@ -54,10 +57,12 @@ Cura.MachineAction ListView { + id: listview model: manager.foundDevices width: parent.width height: 500 currentIndex: activeIndex + onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] delegate: Rectangle { height: childrenRect.height; @@ -68,7 +73,7 @@ Cura.MachineAction anchors.left: parent.left; anchors.leftMargin: UM.Theme.getSize("default_margin").width; anchors.right: parent.right; - text: modelData + text: listview.model[index].name elide: Text.ElideRight } @@ -80,13 +85,64 @@ Cura.MachineAction if(!parent.ListView.isCurrentItem) { parent.ListView.view.currentIndex = index; - //base.itemActivated(); } } } } } } + Column + { + width: parent.width * 0.5 + Label + { + width: parent.width + wrapMode: Text.WordWrap + text: base.selectedPrinter ? base.selectedPrinter.name : "" + font.pointSize: 16; + } + Grid + { + width: parent.width + columns: 2 + Label + { + width: parent.width * 0.5 + wrapMode: Text.WordWrap + text: catalog.i18nc("@label", "Type") + } + Label + { + width: parent.width * 0.5 + wrapMode: Text.WordWrap + text: catalog.i18nc("@label", "Ultimaker 3") + } + Label + { + width: parent.width * 0.5 + wrapMode: Text.WordWrap + text: catalog.i18nc("@label", "Firmware version") + } + Label + { + width: parent.width * 0.5 + wrapMode: Text.WordWrap + text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : "" + } + Label + { + width: parent.width * 0.5 + wrapMode: Text.WordWrap + text: catalog.i18nc("@label", "IP Address") + } + Label + { + width: parent.width * 0.5 + wrapMode: Text.WordWrap + text: base.selectedPrinter ? base.selectedPrinter.ipAddress : "" + } + } + } } } } \ No newline at end of file diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 02646bbba2..d243d62dcc 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -92,6 +92,21 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def getKey(self): return self._key + ## Name of the printer (as returned from the zeroConf properties) + @pyqtProperty(str, constant = True) + def name(self): + return self._properties.get(b"name", b"").decode("utf-8") + + ## Firmware version (as returned from the zeroConf properties) + @pyqtProperty(str, constant=True) + def firmwareVersion(self): + return self._properties.get(b"firmware_version", b"").decode("utf-8") + + ## IPadress of this printer + @pyqtProperty(str, constant=True) + def ipAddress(self): + return self._address + def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") From bb9d3a47ef90a241ed963dd580347c1a63a66818 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 16:15:56 +0200 Subject: [PATCH 067/438] Printer can now actually be linked CURA-336 --- DiscoverUM3Action.py | 9 +++++++++ DiscoverUM3Action.qml | 22 ++++++++++++++++++++++ NetworkPrinterOutputDevice.py | 5 +++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index aade48e806..55afcd35fa 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -30,3 +30,12 @@ class DiscoverUM3Action(MachineAction): return [printers[printer] for printer in printers] else: return [] + + @pyqtSlot(str) + def setKey(self, key): + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack: + if "key" in global_container_stack.getMetaData(): + global_container_stack.setMetaDataEntry("key", key) + else: + global_container_stack.addMetaDataEntry("key", key) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index fcb529536e..704dcc7851 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -145,4 +145,26 @@ Cura.MachineAction } } } + Button + { + text: catalog.i18nc("@action:button", "Ok") + anchors.right: cancelButton.left + anchors.bottom: parent.bottom + onClicked: + { + manager.setKey(base.selectedPrinter.getKey()) + completed() + } + } + Button + { + id: cancelButton + text: catalog.i18nc("@action:button", "Cancel") + anchors.right: discoverUM3Action.right + anchors.bottom: parent.bottom + onClicked: + { + completed() + } + } } \ No newline at end of file diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index d243d62dcc..2d5b2f1b19 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -8,8 +8,8 @@ from UM.Message import Message from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager -from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty -from PyQt5.QtGui import QPixmap, QImage +from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot +from PyQt5.QtGui import QImage import json @@ -89,6 +89,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Get the unique key of this machine # \return key String containing the key of the machine. + @pyqtSlot(result = str) def getKey(self): return self._key From a61ac82433a653962ece793c50366de7a5491b95 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 16:20:20 +0200 Subject: [PATCH 068/438] Linking a key to a printer now re-checks connection CURA-336 --- DiscoverUM3Action.py | 4 ++++ NetworkPrinterOutputDevicePlugin.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 55afcd35fa..b492b79d8b 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -39,3 +39,7 @@ class DiscoverUM3Action(MachineAction): global_container_stack.setMetaDataEntry("key", key) else: global_container_stack.addMetaDataEntry("key", key) + + if self._network_plugin: + # Ensure that the connection states are refreshed. + self._network_plugin.reCheckConnections() diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 6697e05bd9..ad41d984ae 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -19,7 +19,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addPrinterSignal.connect(self.addPrinter) - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) addPrinterSignal = Signal() @@ -34,7 +34,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def getPrinters(self): return self._printers - def _onGlobalStackChanged(self): + def reCheckConnections(self): active_machine = Application.getInstance().getGlobalContainerStack() if not active_machine: return From a1c9f055c868629865674a8c1d8e808d3e0592cc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 16:24:17 +0200 Subject: [PATCH 069/438] UI updates CURA-336 --- DiscoverUM3Action.qml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 704dcc7851..fdb986394e 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -33,11 +33,7 @@ Cura.MachineAction wrapMode: Text.WordWrap text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your ptiner is connected to the network using a network cable of by connecting your printer to your WIFI network. \n\n If you don't want to connect Cura with your Ultimaker 3 now, you can always use a USB drive to transfer g-code files to your Printer.\n\n Select your Ultimaker 3 from the list below:") } - Button - { - text: "Start looking!" - onClicked: manager.startDiscovery() - } + Row { width: parent.width @@ -94,6 +90,7 @@ Cura.MachineAction Column { width: parent.width * 0.5 + visible: base.selectedPrinter Label { width: parent.width @@ -144,6 +141,11 @@ Cura.MachineAction } } } + Button + { + text: catalog.i18nc("@label","Start looking!") + onClicked: manager.startDiscovery() + } } Button { From d6aacf24b95583f3600d3534025377f9822a3a1f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Jun 2016 17:07:20 +0200 Subject: [PATCH 070/438] Added JobName to network output device CURA-1036 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2d5b2f1b19..8ddf318be6 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -260,7 +260,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._updateJobState(json_data["state"]) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) - + self.setJobName(json_data["name"]) elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! self._updateJobState("") From 07c25edb0fad63500bfe401b49270c5cc77feb91 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 24 Jun 2016 13:47:52 +0200 Subject: [PATCH 071/438] Removed unused code --- HttpUploadDataStream.py | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 HttpUploadDataStream.py diff --git a/HttpUploadDataStream.py b/HttpUploadDataStream.py deleted file mode 100644 index a3e5a82ba4..0000000000 --- a/HttpUploadDataStream.py +++ /dev/null @@ -1,32 +0,0 @@ -from UM.Signal import Signal, SignalEmitter -class HttpUploadDataStream(SignalEmitter): - def __init__(self): - super().__init__() - self._data_list = [] - self._total_length = 0 - self._read_position = 0 - - progressSignal = Signal() - - def write(self, data): - data = bytes(data,'UTF-8') - size = len(data) - if size < 1: - return - blocks = int(size / 2048) - for n in range(0, blocks): - self._data_list.append(data[n*2048:n*2048+2048]) - self._data_list.append(data[blocks*2048:]) - self._total_length += size - - def read(self, size): - if self._read_position >= len(self._data_list): - return None - ret = self._data_list[self._read_position] - self._read_position += 1 - - self.progressSignal.emit(float(self._read_position) / float(len(self._data_list))) - return ret - - def __len__(self): - return self._total_length \ No newline at end of file From 260694f2334e1e5cc5fa102cb61583f0b0d85ed9 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 27 Jun 2016 09:58:40 +0200 Subject: [PATCH 072/438] Code style, typos and minute style tweak CURA-336 --- DiscoverUM3Action.qml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index fdb986394e..cf0262abf0 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -31,7 +31,7 @@ Cura.MachineAction id: pageDescription width: parent.width wrapMode: Text.WordWrap - text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your ptiner is connected to the network using a network cable of by connecting your printer to your WIFI network. \n\n If you don't want to connect Cura with your Ultimaker 3 now, you can always use a USB drive to transfer g-code files to your Printer.\n\n Select your Ultimaker 3 from the list below:") + text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. \n\nIf you don't want to connect Cura with your Ultimaker 3 now, you can always use a USB drive to transfer g-code files to your printer.\n\nSelect your Ultimaker 3 from the list below:") } Row @@ -61,15 +61,16 @@ Cura.MachineAction onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] delegate: Rectangle { - height: childrenRect.height; + height: childrenRect.height color: ListView.isCurrentItem ? palette.highlight : index % 2 ? palette.base : palette.alternateBase width: parent.width Label { - anchors.left: parent.left; - anchors.leftMargin: UM.Theme.getSize("default_margin").width; - anchors.right: parent.right; + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right text: listview.model[index].name + color: parent.ListView.isCurrentItem ? palette.highlightedText : palette.text elide: Text.ElideRight } From 0860fc6cf57e529b259056058705249bafe4d834 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 27 Jun 2016 10:32:56 +0200 Subject: [PATCH 073/438] Automatically start discovery CURA-336 --- DiscoverUM3Action.qml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index cf0262abf0..9b1046b445 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -59,6 +59,7 @@ Cura.MachineAction height: 500 currentIndex: activeIndex onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] + Component.onCompleted: manager.startDiscovery() delegate: Rectangle { height: childrenRect.height @@ -142,11 +143,6 @@ Cura.MachineAction } } } - Button - { - text: catalog.i18nc("@label","Start looking!") - onClicked: manager.startDiscovery() - } } Button { From 7af7748fff323449f5b48758c462c206757efeb8 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 27 Jun 2016 14:40:07 +0200 Subject: [PATCH 074/438] Make machine action button translatable CURA-336 --- DiscoverUM3Action.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index b492b79d8b..2ee14b4b9d 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -4,9 +4,12 @@ from UM.Application import Application from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + class DiscoverUM3Action(MachineAction): def __init__(self): - super().__init__("DiscoverUM3Action", "Discover printers") + super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network")) self._qml_url = "DiscoverUM3Action.qml" self._network_plugin = None From 8deb0dc2743d1d85899cb636b88ed831c05838a9 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 27 Jun 2016 14:40:07 +0200 Subject: [PATCH 075/438] Make machine action button translatable CURA-336 --- DiscoverUM3Action.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index b492b79d8b..2ee14b4b9d 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -4,9 +4,12 @@ from UM.Application import Application from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + class DiscoverUM3Action(MachineAction): def __init__(self): - super().__init__("DiscoverUM3Action", "Discover printers") + super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network")) self._qml_url = "DiscoverUM3Action.qml" self._network_plugin = None From 53c1c738d539b85256e670d80443cfcb1a82c74f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 27 Jun 2016 16:51:29 +0200 Subject: [PATCH 076/438] Fix error CURA-49 --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 8ddf318be6..72dff3e275 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -257,13 +257,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if progress == 0: progress += 0.001 self.setProgress(progress) - self._updateJobState(json_data["state"]) + self._setJobState(json_data["state"]) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) self.setJobName(json_data["name"]) elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! - self._updateJobState("") + self._setJobState("") elif "snapshot" in reply.url().toString(): # Status update from image: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) From b8fddf6527d4096d8b43784a86c2bb99ab1115f5 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 27 Jun 2016 17:49:47 +0200 Subject: [PATCH 077/438] Tweak codestyle and styling CURA-336 --- DiscoverUM3Action.qml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 9b1046b445..d74a39ade7 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -16,14 +16,14 @@ Cura.MachineAction anchors.fill: parent; id: discoverUM3Action SystemPalette { id: palette } - UM.I18nCatalog { id: catalog; name:"cura"} + UM.I18nCatalog { id: catalog; name:"cura" } Label { id: pageTitle width: parent.width text: catalog.i18nc("@title", "Connect to Networked Printer") wrapMode: Text.WordWrap - font.pointSize: 18; + font.pointSize: 18 } Label @@ -41,8 +41,9 @@ Cura.MachineAction ScrollView { id: objectListContainer - frameVisible: true; + frameVisible: true width: parent.width * 0.5 + height: base.height - parent.y Rectangle { @@ -56,7 +57,6 @@ Cura.MachineAction id: listview model: manager.foundDevices width: parent.width - height: 500 currentIndex: activeIndex onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] Component.onCompleted: manager.startDiscovery() @@ -98,7 +98,8 @@ Cura.MachineAction width: parent.width wrapMode: Text.WordWrap text: base.selectedPrinter ? base.selectedPrinter.name : "" - font.pointSize: 16; + font: UM.Theme.getFont("large") + elide: Text.ElideRight } Grid { From 76dfb5e718449c7d5e960a0e7ef14aa1b34f2f5a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 11:48:54 +0200 Subject: [PATCH 078/438] Renamed generic key to um_network_key CURA-49 --- DiscoverUM3Action.py | 4 ++-- NetworkPrinterOutputDevicePlugin.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index b492b79d8b..4415b5f20a 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -36,9 +36,9 @@ class DiscoverUM3Action(MachineAction): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: if "key" in global_container_stack.getMetaData(): - global_container_stack.setMetaDataEntry("key", key) + global_container_stack.setMetaDataEntry("um_network_key", key) else: - global_container_stack.addMetaDataEntry("key", key) + global_container_stack.addMetaDataEntry("um_network_key", key) if self._network_plugin: # Ensure that the connection states are refreshed. diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index ad41d984ae..cee9e5654c 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -40,7 +40,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): return for key in self._printers: - if key == active_machine.getMetaDataEntry("key"): + if key == active_machine.getMetaDataEntry("um_network_key"): self._printers[key].connect() self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) else: @@ -51,7 +51,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) self._printers[printer.getKey()] = printer global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("key"): + if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"): self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) From 2711a9bc95bf1cb73697dd8f76b970632776f617 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 11:52:55 +0200 Subject: [PATCH 079/438] Changed name of plugin to better describe what it's doing CURA-49 --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 711b7caf93..b030fe61e6 100644 --- a/__init__.py +++ b/__init__.py @@ -8,9 +8,9 @@ catalog = i18nCatalog("cura") def getMetaData(): return { "plugin": { - "name": "Wifi connection", + "name": "UM3 Network Connection", "author": "Ultimaker", - "description": catalog.i18nc("Wifi connection", "Wifi connection"), + "description": catalog.i18nc("Wifi connection", "UM3 Network Connection"), "api": 3 } } From 5e51d4d998dcb709a4dc0011e76d5b6a9ee2cef9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 12:59:37 +0200 Subject: [PATCH 080/438] Minor refactor to make code more readable CURA-336 --- DiscoverUM3Action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 51b5253ea2..8fed85d3e1 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -30,7 +30,7 @@ class DiscoverUM3Action(MachineAction): def foundDevices(self): if self._network_plugin: printers = self._network_plugin.getPrinters() - return [printers[printer] for printer in printers] + return list(printers.values()) else: return [] From ab934d356b073c972755555348b8474e8b7426ef Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 13:46:20 +0200 Subject: [PATCH 081/438] Expanded logging --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 72dff3e275..e3844efb2d 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -275,7 +275,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 204: pass # Request was sucesfull! else: - Logger.log("d","Something went wrong when trying to update data of API. %s statuscode: %s", reply.readAll(), reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) + Logger.log("d", "Something went wrong when trying to update data of API (%s). Message: %s Statuscode: %s", reply.url().toString(), reply.readAll(), reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) else: Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) From e53ff78704fcd1b5b3ccfa8133b50400bd309683 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 13:51:19 +0200 Subject: [PATCH 082/438] Revert "Fix error" This reverts commit 53c1c738d539b85256e670d80443cfcb1a82c74f. The code was intentional. The fix actually caused issues, as it sent data it got from the server back again --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e3844efb2d..3e3b9791c5 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -257,13 +257,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if progress == 0: progress += 0.001 self.setProgress(progress) - self._setJobState(json_data["state"]) + self._updateJobState(json_data["state"]) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) self.setJobName(json_data["name"]) elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: self.setProgress(0) # No print job found, so there can't be progress! - self._setJobState("") + self._updateJobState("") elif "snapshot" in reply.url().toString(): # Status update from image: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) From 4c7d235a0d6ecd8dd21edda72d69da3c6289a69c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 14:50:55 +0200 Subject: [PATCH 083/438] Fixed mistake with updating linked key --- DiscoverUM3Action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 8fed85d3e1..27c5ef6638 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -38,7 +38,7 @@ class DiscoverUM3Action(MachineAction): def setKey(self, key): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: - if "key" in global_container_stack.getMetaData(): + if "um_network_key" in global_container_stack.getMetaData(): global_container_stack.setMetaDataEntry("um_network_key", key) else: global_container_stack.addMetaDataEntry("um_network_key", key) From bc50f0fa7cdb5ef04cefb7790ac4a249ad5f69b4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 15:00:35 +0200 Subject: [PATCH 084/438] Only close connection if it's open when re-checking connections --- NetworkPrinterOutputDevicePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index cee9e5654c..4055b59a1a 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -44,7 +44,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._printers[key].connect() self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged) else: - self._printers[key].close() + if self._printers[key].isConnected(): + self._printers[key].close() ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addPrinter(self, name, address, properties): From 15e11e1cadae97927c7341ba6f83dee6f3c42440 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 16:37:37 +0200 Subject: [PATCH 085/438] Added basic authentication for UM3 Note that the authentication is not saved at the moment, so this step needs to be repeated on every boot CURA-49 --- NetworkPrinterOutputDevice.py | 47 +++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 3e3b9791c5..b0bd37c64a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -48,6 +48,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) + self._manager.authenticationRequired.connect(self._onAuthenticationRequired) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None @@ -82,8 +83,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_image_id = 0 + self._authenticated = False + self._authentication_id = None + self._authentication_key = None + self._camera_image = QImage() + def _onAuthenticationRequired(self, reply, authenticator): + if self._authentication_id is not None and self._authentication_key is not None: + authenticator.setUser(self._authentication_id) + authenticator.setPassword(self._authentication_key) + def getProperties(self): return self._properties @@ -115,6 +125,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._image_reply = self._manager.get(self._image_request) def _update(self): + if not self._authenticated: + self._checkAuthentication() ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") self._printer_request = QNetworkRequest(url) @@ -237,6 +249,19 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e)) + ## Verify if we are authenticated to make requests. + def _checkAuthentication(self): + url = QUrl("http://" + self._address + self._api_prefix + "auth/verify") + request = QNetworkRequest(url) + self._manager.get(request) + + ## Request a authentication key from the printer so we can be authenticated + def _requestAuthentication(self): + url = QUrl("http://" + self._address + self._api_prefix + "auth/request") + request = QNetworkRequest(url) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self._manager.post(request, json.dumps({"application": "Cura", "user":"test"}).encode()) + ## Handler for all requests that have finished. def _onFinished(self, reply): if reply.operation() == QNetworkAccessManager.GetOperation: @@ -268,9 +293,27 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() + elif "auth/verify" in reply.url().toString(): # Answer when requesting authentication + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 401: + Logger.log("i", "Not authenticated. Attempting to request authentication") + self._requestAuthentication() + else: + self._authenticated = True + Logger.log("i", "Authentication succeeded") + elif reply.operation() == QNetworkAccessManager.PostOperation: - reply.uploadProgress.disconnect(self._onUploadProgress) - self._progress_message.hide() + if "/auth/request" in reply.url().toString(): + # We got a response to requesting authentication. + data = json.loads(bytes(reply.readAll()).decode("utf-8")) + self._authentication_key = data["key"] + self._authentication_id = data["id"] + Logger.log("i", "Got a new authentication ID. Waiting for authorization: %s", self._authentication_id ) + + # Continue with handshaking; send request to printer so it can be authenticated. + self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id)))) + else: + reply.uploadProgress.disconnect(self._onUploadProgress) + self._progress_message.hide() elif reply.operation() == QNetworkAccessManager.PutOperation: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 204: pass # Request was sucesfull! From 6244d74c1d0a15150ea81cf2ea4dabcd5b5db518 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 16:45:02 +0200 Subject: [PATCH 086/438] Code cleanup --- NetworkPrinterOutputDevice.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index b0bd37c64a..414861b38b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -50,24 +50,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._manager.finished.connect(self._onFinished) self._manager.authenticationRequired.connect(self._onAuthenticationRequired) - ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) - self._printer_request = None - self._printer_reply = None - - self._print_job_request = None - self._print_job_reply = None - - self._image_request = None - self._image_reply = None - self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None - self._put_request = None - self._put_reply = None - self._progress_message = None self._error_message = None @@ -121,21 +108,21 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") - self._image_request = QNetworkRequest(url) - self._image_reply = self._manager.get(self._image_request) + image_request = QNetworkRequest(url) + self._manager.get(image_request) def _update(self): if not self._authenticated: self._checkAuthentication() ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") - self._printer_request = QNetworkRequest(url) - self._printer_reply = self._manager.get(self._printer_request) + printer_request = QNetworkRequest(url) + self._manager.get(printer_request) ## Request print_job data url = QUrl("http://" + self._address + self._api_prefix + "print_job") - self._print_job_request = QNetworkRequest(url) - self._print_job_reply = self._manager.get(self._print_job_request) + print_job_request = QNetworkRequest(url) + self._manager.get(print_job_request) ## Convenience function that gets information from the received json data and converts it to the right internal # values / variables @@ -200,10 +187,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _setJobState(self, job_state): url = QUrl("http://" + self._address + self._api_prefix + "print_job/state") - self._put_request = QNetworkRequest(url) - self._put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + put_request = QNetworkRequest(url) + put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"target\": \"%s\"}" % job_state - self._put_reply = self._manager.put(self._put_request, data.encode()) + self._manager.put(put_request, data.encode()) def startPrint(self): if self._progress != 0: From c13a6966d0f93b0f37267e6d8c1efa2d7f06e7f3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Jun 2016 17:01:33 +0200 Subject: [PATCH 087/438] Handle authentication denied messages CURA-49 --- NetworkPrinterOutputDevice.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 414861b38b..632c4c6d43 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -15,6 +15,13 @@ import json i18n_catalog = i18nCatalog("cura") +from enum import IntEnum + +class AuthState(IntEnum): + NotAuthenticated = 1 + AuthenticationRequested = 2 + Authenticated = 3 + AuthenticationDenied = 4 ## Network connected (wifi / lan) printer that uses the Ultimaker API @signalemitter @@ -70,7 +77,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_image_id = 0 - self._authenticated = False + self._authentication_state = AuthState.NotAuthenticated self._authentication_id = None self._authentication_key = None @@ -112,7 +119,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._manager.get(image_request) def _update(self): - if not self._authenticated: + if self._authentication_state in [AuthState.NotAuthenticated, AuthState.AuthenticationRequested]: self._checkAuthentication() ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") @@ -247,6 +254,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): url = QUrl("http://" + self._address + self._api_prefix + "auth/request") request = QNetworkRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self._authentication_state = AuthState.AuthenticationRequested self._manager.post(request, json.dumps({"application": "Cura", "user":"test"}).encode()) ## Handler for all requests that have finished. @@ -285,8 +293,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("i", "Not authenticated. Attempting to request authentication") self._requestAuthentication() else: - self._authenticated = True + self._authenticated = AuthState.Authenticated Logger.log("i", "Authentication succeeded") + elif "auth/check" in reply.url().toString(): # Check if we are authenticated (user can refuse this!) + data = json.loads(bytes(reply.readAll()).decode("utf-8")) + if data.get("message", "") == "authorized": + Logger.log("i", "Authentication completed.") + self._authentication_state = AuthState.Authenticated + else: + Logger.log("i", "Authentication was denied.") + self._authentication_state = AuthState.AuthenticationDenied elif reply.operation() == QNetworkAccessManager.PostOperation: if "/auth/request" in reply.url().toString(): From c8ba60fad4bb161016fd7a44bb85238b1aae00dc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 10:18:02 +0200 Subject: [PATCH 088/438] Authentication message is now shown when authenticating CURA-49 --- NetworkPrinterOutputDevice.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 632c4c6d43..02334a3c4b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -81,6 +81,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_id = None self._authentication_key = None + self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please aprove the request on the printer"), lifetime = 0, dismissable = False) self._camera_image = QImage() def _onAuthenticationRequired(self, reply, authenticator): @@ -118,6 +119,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): image_request = QNetworkRequest(url) self._manager.get(image_request) + def setAuthenticationState(self, auth_state): + if auth_state == AuthState.AuthenticationRequested: + self._authentication_requested_message.show() + elif auth_state == AuthState.Authenticated: + self._authentication_requested_message.hide() + elif auth_state == AuthState.AuthenticationDenied: + self._authentication_requested_message.hide() + + self._authentication_state = auth_state + def _update(self): if self._authentication_state in [AuthState.NotAuthenticated, AuthState.AuthenticationRequested]: self._checkAuthentication() @@ -254,7 +265,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): url = QUrl("http://" + self._address + self._api_prefix + "auth/request") request = QNetworkRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._authentication_state = AuthState.AuthenticationRequested + self.setAuthenticationState(AuthState.AuthenticationRequested) self._manager.post(request, json.dumps({"application": "Cura", "user":"test"}).encode()) ## Handler for all requests that have finished. @@ -290,19 +301,25 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.newImage.emit() elif "auth/verify" in reply.url().toString(): # Answer when requesting authentication if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 401: - Logger.log("i", "Not authenticated. Attempting to request authentication") - self._requestAuthentication() + if self._authentication_state != AuthState.AuthenticationRequested: + # Only request a new authentication when we have not already done so. + Logger.log("i", "Not authenticated. Attempting to request authentication") + self._requestAuthentication() + elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 403: + print("403") else: - self._authenticated = AuthState.Authenticated + self.setAuthenticationState(AuthState.Authenticated) Logger.log("i", "Authentication succeeded") elif "auth/check" in reply.url().toString(): # Check if we are authenticated (user can refuse this!) data = json.loads(bytes(reply.readAll()).decode("utf-8")) + print("auth check response") if data.get("message", "") == "authorized": Logger.log("i", "Authentication completed.") - self._authentication_state = AuthState.Authenticated + self.setAuthenticationState(AuthState.Authenticated) else: - Logger.log("i", "Authentication was denied.") - self._authentication_state = AuthState.AuthenticationDenied + pass + #Logger.log("i", "Authentication was denied.") + #self.setAuthenticationState(AuthState.AuthenticationDenied) elif reply.operation() == QNetworkAccessManager.PostOperation: if "/auth/request" in reply.url().toString(): From 3a767c72f07cf59bafe178dc7259df3a93167ecb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 10:55:45 +0200 Subject: [PATCH 089/438] Authentication can now timeout and be denied from printer side CURA-49 --- NetworkPrinterOutputDevice.py | 40 ++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 02334a3c4b..9987fcda36 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -77,13 +77,28 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_image_id = 0 + self._authentication_counter = 0 + self._max_authentication_counter = 30 # Number of attempts before authentication timed out. + + self._authentication_timer = QTimer() + self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval + self._authentication_timer.setSingleShot(False) + self._authentication_timer.timeout.connect(self._onAuthenticationTimer) + self._authentication_state = AuthState.NotAuthenticated self._authentication_id = None self._authentication_key = None - self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please aprove the request on the printer"), lifetime = 0, dismissable = False) + self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please aprove the request on the printer"), lifetime = 0, dismissable = False, progress = 0) self._camera_image = QImage() + def _onAuthenticationTimer(self): + self._authentication_counter += 1 + self._authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter * 100) + if self._authentication_counter > self._max_authentication_counter: + self._authentication_timer.stop() + self.setAuthenticationState(AuthState.AuthenticationDenied) + def _onAuthenticationRequired(self, reply, authenticator): if self._authentication_id is not None and self._authentication_key is not None: authenticator.setUser(self._authentication_id) @@ -122,6 +137,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def setAuthenticationState(self, auth_state): if auth_state == AuthState.AuthenticationRequested: self._authentication_requested_message.show() + self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: self._authentication_requested_message.hide() elif auth_state == AuthState.AuthenticationDenied: @@ -130,8 +146,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_state = auth_state def _update(self): - if self._authentication_state in [AuthState.NotAuthenticated, AuthState.AuthenticationRequested]: - self._checkAuthentication() + if self._authentication_state == AuthState.NotAuthenticated: + self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. + elif self._authentication_state == AuthState.AuthenticationRequested: + self._checkAuthentication() # We requested authentication at some point. Check if we got permission. ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") printer_request = QNetworkRequest(url) @@ -170,7 +188,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check if cartridges are loaded at all (Error) #self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"] != "" - # Check if there is material loaded at all (Error) + # Check if there is material loaded at all (Error)self.authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter) #self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"] != "" # Check if there is enough material (Warning) @@ -255,11 +273,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("e", "An exception occurred in network connection: %s" % str(e)) ## Verify if we are authenticated to make requests. - def _checkAuthentication(self): + def _verifyAuthentication(self): url = QUrl("http://" + self._address + self._api_prefix + "auth/verify") request = QNetworkRequest(url) self._manager.get(request) + def _checkAuthentication(self): + self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id)))) + ## Request a authentication key from the printer so we can be authenticated def _requestAuthentication(self): url = QUrl("http://" + self._address + self._api_prefix + "auth/request") @@ -306,7 +327,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("i", "Not authenticated. Attempting to request authentication") self._requestAuthentication() elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 403: - print("403") + pass else: self.setAuthenticationState(AuthState.Authenticated) Logger.log("i", "Authentication succeeded") @@ -316,6 +337,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if data.get("message", "") == "authorized": Logger.log("i", "Authentication completed.") self.setAuthenticationState(AuthState.Authenticated) + elif data.get("message", "") == "unauthorized": + Logger.log("i", "Authentication was denied.") + self.setAuthenticationState(AuthState.AuthenticationDenied) else: pass #Logger.log("i", "Authentication was denied.") @@ -329,8 +353,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_id = data["id"] Logger.log("i", "Got a new authentication ID. Waiting for authorization: %s", self._authentication_id ) - # Continue with handshaking; send request to printer so it can be authenticated. - self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id)))) + # Check if the authentication is accepted. + self._checkAuthentication() else: reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() From cba372737333e70139fcdbb7e4e9ea23d1ff7c56 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 29 Jun 2016 11:24:25 +0200 Subject: [PATCH 090/438] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e0573ec934..8aea59b4ff 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # JediWifiPrintingPlugin Secret plugin to enable wifi printing from Cura to JediPrinter + + +Simply copy this into [Cura installation folder]/plugins/JediWifiPrintingPlugin From a90be88a0420bc14e7d69f539c1b020c66870d2e Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 29 Jun 2016 11:34:02 +0200 Subject: [PATCH 091/438] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8aea59b4ff..40a21e8abb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # JediWifiPrintingPlugin Secret plugin to enable wifi printing from Cura to JediPrinter - -Simply copy this into [Cura installation folder]/plugins/JediWifiPrintingPlugin +Intructions +---- +- Clone repo into [Cura installation folder]/plugins/JediWifiPrintingPlugin (Or somewhere else and add a link..) +- sudo apt-get install python3-zeroconf +- sudo apt-get install python-zeroconf # is this one needed? From c3ecacf6c091a4e5daeb25519bf89affe80dbd30 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 29 Jun 2016 11:40:32 +0200 Subject: [PATCH 092/438] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 40a21e8abb..33aa76c306 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,3 @@ Intructions ---- - Clone repo into [Cura installation folder]/plugins/JediWifiPrintingPlugin (Or somewhere else and add a link..) - sudo apt-get install python3-zeroconf -- sudo apt-get install python-zeroconf # is this one needed? From 054f7aaa8315c25de9d0617000654c965bf880a5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 11:53:56 +0200 Subject: [PATCH 093/438] If authentication is recieved, it's now also correctly validated CURA-49 --- NetworkPrinterOutputDevice.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 9987fcda36..15f33aedde 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -335,15 +335,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): data = json.loads(bytes(reply.readAll()).decode("utf-8")) print("auth check response") if data.get("message", "") == "authorized": - Logger.log("i", "Authentication completed.") - self.setAuthenticationState(AuthState.Authenticated) + Logger.log("i", "Authentication was approved") + self._verifyAuthentication() # Ensure that the verification is really used and correct. elif data.get("message", "") == "unauthorized": Logger.log("i", "Authentication was denied.") self.setAuthenticationState(AuthState.AuthenticationDenied) else: pass - #Logger.log("i", "Authentication was denied.") - #self.setAuthenticationState(AuthState.AuthenticationDenied) elif reply.operation() == QNetworkAccessManager.PostOperation: if "/auth/request" in reply.url().toString(): From 4b6993bf7bdf635ecdfa9795b1ac63b85650bba0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 12:00:28 +0200 Subject: [PATCH 094/438] Message is now shown when trying to print without authentication CURA-49 --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 15f33aedde..5f6f0f9378 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -233,6 +233,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) self._error_message.show() return + elif self._authentication_state != AuthState.Authenticated: + self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", + "Not authenticated to print with this machine. Unable to start a new job.")) + self._not_authenticated_message.show() + return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() @@ -333,7 +338,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("i", "Authentication succeeded") elif "auth/check" in reply.url().toString(): # Check if we are authenticated (user can refuse this!) data = json.loads(bytes(reply.readAll()).decode("utf-8")) - print("auth check response") if data.get("message", "") == "authorized": Logger.log("i", "Authentication was approved") self._verifyAuthentication() # Ensure that the verification is really used and correct. From dd92ac7c5f626da4fe83237397f1f2be7ab25270 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 12:06:36 +0200 Subject: [PATCH 095/438] Username & cura version are now sent in the auth request CURA-49 --- NetworkPrinterOutputDevice.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 5f6f0f9378..f7e7c0777b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -12,6 +12,7 @@ from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot from PyQt5.QtGui import QImage import json +import os i18n_catalog = i18nCatalog("cura") @@ -228,6 +229,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): data = "{\"target\": \"%s\"}" % job_state self._manager.put(put_request, data.encode()) + def _getUserName(self): + for name in ('LOGNAME', 'USER', 'LNAME', 'USERNAME'): + user = os.environ.get(name) + if user: + return user + return "Unknown User" # Couldn't find out username. + def startPrint(self): if self._progress != 0: self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) @@ -292,7 +300,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): request = QNetworkRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") self.setAuthenticationState(AuthState.AuthenticationRequested) - self._manager.post(request, json.dumps({"application": "Cura", "user":"test"}).encode()) + self._manager.post(request, json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName()}).encode()) ## Handler for all requests that have finished. def _onFinished(self, reply): From acd3274ca44b8fdc8999f06174c97e72d0fe36c9 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 29 Jun 2016 12:07:25 +0200 Subject: [PATCH 096/438] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33aa76c306..277111412c 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ Secret plugin to enable wifi printing from Cura to JediPrinter Intructions ---- - Clone repo into [Cura installation folder]/plugins/JediWifiPrintingPlugin (Or somewhere else and add a link..) -- sudo apt-get install python3-zeroconf +- pip3 install python3-zeroconf From 368d4e9ea2af93469a6a066d7ad2aee0e389567b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 12:12:12 +0200 Subject: [PATCH 097/438] Updated documentation CURA-49 --- NetworkPrinterOutputDevice.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f7e7c0777b..2dca836fb6 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -135,6 +135,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): image_request = QNetworkRequest(url) self._manager.get(image_request) + ## Set the authentication state. + # \param auth_state \type{AuthState} Enum value representing the new auth state def setAuthenticationState(self, auth_state): if auth_state == AuthState.AuthenticationRequested: self._authentication_requested_message.show() @@ -146,6 +148,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_state = auth_state + ## Request data from the connected device. def _update(self): if self._authentication_state == AuthState.NotAuthenticated: self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. @@ -229,6 +232,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): data = "{\"target\": \"%s\"}" % job_state self._manager.put(put_request, data.encode()) + ## Convenience function to get the username from the OS. + # The code was copied from the getpass module, as we try to use as little dependencies as possible. def _getUserName(self): for name in ('LOGNAME', 'USER', 'LNAME', 'USERNAME'): user = os.environ.get(name) @@ -236,6 +241,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return user return "Unknown User" # Couldn't find out username. + ## Attempt to start a new print. + # This function can fail to actually start a print due to not being authenticated or another print already + # being in progress. def startPrint(self): if self._progress != 0: self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) @@ -291,6 +299,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): request = QNetworkRequest(url) self._manager.get(request) + ## Check if the authentication request was allowed by the printer. def _checkAuthentication(self): self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id)))) From 5597f6dcf2bc3c9cc6d1c89d62dd20f945546cc4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 13:19:05 +0200 Subject: [PATCH 098/438] Authentication is now saved with machine-instance CURA-49 --- NetworkPrinterOutputDevice.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2dca836fb6..9bb77c46e1 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -211,6 +211,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) + + ## Check if this machine was authenticated before. + self._authentication_id = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_id", None) + self._authentication_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_key", None) + self._update_timer.start() self._camera_timer.start() @@ -352,6 +357,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass else: self.setAuthenticationState(AuthState.Authenticated) + global_container_stack = Application.getInstance().getGlobalContainerStack() + ## Save authentication details. + if global_container_stack: + if "network_authentication_key" in global_container_stack.getMetaData(): + global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key) + else: + global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key) + if "network_authentication_id" in global_container_stack.getMetaData(): + global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id) + else: + global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id) Logger.log("i", "Authentication succeeded") elif "auth/check" in reply.url().toString(): # Check if we are authenticated (user can refuse this!) data = json.loads(bytes(reply.readAll()).decode("utf-8")) From 58c3f5dc89a1e364d58c59aaf0d141195b1825c5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 13:31:42 +0200 Subject: [PATCH 099/438] Closing the networkprinteroutput device now hides all it's messages CURA-49 --- NetworkPrinterOutputDevice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 9bb77c46e1..94ede5add3 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -182,6 +182,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): self.setConnectionState(ConnectionState.closed) + self._progress_message.hide() + self._authentication_requested_message.hide() + self._error_message.hide() + self._authentication_counter = 0 + self._authentication_timer.stop() self._update_timer.stop() self._camera_timer.stop() From 57ea4f0dc8b3d483e3b15a59018930e885ceb6b9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 13:33:31 +0200 Subject: [PATCH 100/438] Updated auth timeout to 5 min --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 94ede5add3..0b8fe607e5 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -79,7 +79,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_image_id = 0 self._authentication_counter = 0 - self._max_authentication_counter = 30 # Number of attempts before authentication timed out. + self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min) self._authentication_timer = QTimer() self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval From 12dcc43baca449c2b62c9f2652451ca6b8ef37dd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 29 Jun 2016 13:45:29 +0200 Subject: [PATCH 101/438] Added messages upon succesfull & not sucessfull pairing CURA-49 --- NetworkPrinterOutputDevice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 0b8fe607e5..f62fc6df5a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -143,9 +143,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: self._authentication_requested_message.hide() + authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) + authentication_succeeded_message.show() elif auth_state == AuthState.AuthenticationDenied: self._authentication_requested_message.hide() - + authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed. This can be either due to a timeout or the printer refused the request.")) + authentication_failed_message.show() self._authentication_state = auth_state ## Request data from the connected device. From b9f73a8eefb96507cb6b076e32eb33bf0c14f079 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Jun 2016 09:35:57 +0200 Subject: [PATCH 102/438] Hiding messages is now only done when there are messages CURA-49 --- NetworkPrinterOutputDevice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f62fc6df5a..ebe850e323 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -185,9 +185,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): self.setConnectionState(ConnectionState.closed) - self._progress_message.hide() + if self._progress_message: + self._progress_message.hide() self._authentication_requested_message.hide() - self._error_message.hide() + if self._error_message: + self._error_message.hide() self._authentication_counter = 0 self._authentication_timer.stop() self._update_timer.stop() From a4117bd3bebbb1edf4be8727c59f181e324ee701 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Jun 2016 09:52:48 +0200 Subject: [PATCH 103/438] Display data is reset upon job completion --- NetworkPrinterOutputDevice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index ebe850e323..0c8805e026 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -351,8 +351,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setTimeTotal(json_data["time_total"]) self.setJobName(json_data["name"]) elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: - self.setProgress(0) # No print job found, so there can't be progress! + self.setProgress(0) # No print job found, so there can't be progress or other data. self._updateJobState("") + self.setTimeElapsed(0) + self.setTimeTotal(0) + self.setJobName("") elif "snapshot" in reply.url().toString(): # Status update from image: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) From 79ee928883c76773fd34bc55cd769cf031d109b1 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 30 Jun 2016 09:54:08 +0200 Subject: [PATCH 104/438] Change progress to percentages CURA-1036 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f62fc6df5a..b616613dce 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -343,7 +343,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## If progress is 0 add a bit so another print can't be sent. if progress == 0: progress += 0.001 - self.setProgress(progress) + self.setProgress(progress * 100) self._updateJobState(json_data["state"]) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) From ca79e398c88524dd35ec1e2557ff4971c4025d3c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Jun 2016 14:25:54 +0200 Subject: [PATCH 105/438] All known materials are now send upon connection Due to garbage collection issues, the (multi) part requests are now cached. CURA-334 --- NetworkPrinterOutputDevice.py | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 0c8805e026..f82da80d4c 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -5,6 +5,8 @@ from UM.Signal import signalemitter from UM.Message import Message +import UM.Settings + from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager @@ -63,6 +65,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_multi_part = None self._post_part = None + + self._material_multi_part = None + self._material_part = None + self._progress_message = None self._error_message = None @@ -93,6 +99,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please aprove the request on the printer"), lifetime = 0, dismissable = False, progress = 0) self._camera_image = QImage() + self._material_post_objects = {} + def _onAuthenticationTimer(self): self._authentication_counter += 1 self._authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter * 100) @@ -145,6 +153,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message.hide() authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) authentication_succeeded_message.show() + # Once we are authenticated we need to send all material profiles. + # + self.sendMaterialProfiles() elif auth_state == AuthState.AuthenticationDenied: self._authentication_requested_message.hide() authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed. This can be either due to a timeout or the printer refused the request.")) @@ -326,6 +337,32 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setAuthenticationState(AuthState.AuthenticationRequested) self._manager.post(request, json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName()}).encode()) + ## Send all material profiles to the printer. + def sendMaterialProfiles(self): + for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material"): + try: + xml_data = container.serialize() + if xml_data == "": + continue + material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) + + material_part = QHttpPart() + file_name = "none.xml" + material_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\";filename=\"%s\"" % file_name) + material_part.setBody(xml_data.encode()) + material_multi_part.append(material_part) + url = QUrl("http://" + self._address + self._api_prefix + "materials") + material_post_request = QNetworkRequest(url) + + self._manager.post(material_post_request, material_multi_part) + + # Keep reference to material_part and material_multi_part so the garbage collector won't touch them. + self._material_post_objects[container.getId()] = (material_part, material_multi_part) + except NotImplementedError: + # If the material container is not the most "generic" one it can't be serialized an will raise a + # NotImplementedError. We can simply ignore these. + pass + ## Handler for all requests that have finished. def _onFinished(self, reply): if reply.operation() == QNetworkAccessManager.GetOperation: @@ -403,6 +440,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check if the authentication is accepted. self._checkAuthentication() + elif "materials" in reply.url().toString(): + # TODO: Remove cached post request items. + pass else: reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() From bfc7a7e3c956d9df7df11aa82f9aaf8cf27d47ea Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Jun 2016 14:56:36 +0200 Subject: [PATCH 106/438] Multipart data objects used for posting new materials are now deleted upon completion CURA-344 --- NetworkPrinterOutputDevice.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f82da80d4c..3fc22ad4d7 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -353,11 +353,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): material_multi_part.append(material_part) url = QUrl("http://" + self._address + self._api_prefix + "materials") material_post_request = QNetworkRequest(url) + reply = self._manager.post(material_post_request, material_multi_part) - self._manager.post(material_post_request, material_multi_part) - - # Keep reference to material_part and material_multi_part so the garbage collector won't touch them. - self._material_post_objects[container.getId()] = (material_part, material_multi_part) + # Keep reference to material_part, material_multi_part and reply so the garbage collector won't touch them. + self._material_post_objects[id(reply)] = (material_part, material_multi_part, reply) except NotImplementedError: # If the material container is not the most "generic" one it can't be serialized an will raise a # NotImplementedError. We can simply ignore these. @@ -441,8 +440,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check if the authentication is accepted. self._checkAuthentication() elif "materials" in reply.url().toString(): - # TODO: Remove cached post request items. - pass + # Remove cached post request items. + del self._material_post_objects[id(reply)] else: reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() From ea3a039bdd991c3bb39ef7d9e69720e00db42748 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 1 Jul 2016 15:07:17 +0200 Subject: [PATCH 107/438] Automatically show the Print Monitor when starting a print CURA-1036 --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index af8bf174cc..40f12fd61d 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -196,6 +196,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_timer.stop() def requestWrite(self, node, file_name = None, filter_by_machine = False): + Application.getInstance().showPrintMonitor.emit(True) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") # TODO: Implement all checks. From 051419577e59ade23ee5bcd259d96cf972d8f91b Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 4 Jul 2016 11:58:31 +0200 Subject: [PATCH 108/438] Forward hotend and material data to PrinterOutputDevice CURA-491 --- NetworkPrinterOutputDevice.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index dd7bdbe49e..465889973e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -44,6 +44,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## It's okay to leave this for now, as this plugin is um3 only (and has 2 extruders by definition) self._num_extruders = 2 + # These are reinitialised here (from PrinterOutputDevice) to match the new _num_extruders self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders @@ -185,6 +186,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): for index in range(0, self._num_extruders): temperature = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["temperature"]["current"] self._setHotendTemperature(index, temperature) + try: + material_id = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] + except KeyError: + material_id = "" + self._setMaterialId(index, material_id) + try: + hotend_id = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] + except KeyError: + hotend_id = "" + self._setHotendId(index, hotend_id) bed_temperature = self._json_printer_state["bed"]["temperature"]["current"] self._setBedTemperature(bed_temperature) @@ -214,7 +225,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check if cartridges are loaded at all (Error) #self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"] != "" - # Check if there is material loaded at all (Error)self.authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter) + # Check if there is material loaded at all (Error) #self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"] != "" # Check if there is enough material (Warning) From 5b686f0a5035fcb4cdee30d43e7ead6fe27e4feb Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 4 Jul 2016 14:26:54 +0200 Subject: [PATCH 109/438] Reinitialise material and hotend lists to new number of extruders CURA-491 --- NetworkPrinterOutputDevice.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 465889973e..928332808a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -48,6 +48,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders + self._material_ids = [""] * self._num_extruders + self._hotend_ids = [""] * self._num_extruders + self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" self.setName(key) From fe4ed3dad82a4c1a41b6428d2e0f45e21f97c1be Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 4 Jul 2016 18:18:42 +0200 Subject: [PATCH 110/438] Use jobname instead of "test" placeholder CURA-49 --- NetworkPrinterOutputDevice.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index dd7bdbe49e..0e14c2985a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -290,8 +290,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): for line in self._gcode: single_string_file_data += line - ## TODO: Use correct file name (we use placeholder now) - file_name = "test.gcode" + file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) From 4fec82fab73c61ad32834dd006373bcde2298796 Mon Sep 17 00:00:00 2001 From: awhiemstra Date: Tue, 5 Jul 2016 10:31:15 +0200 Subject: [PATCH 111/438] Fix CMakeLists to include the discovery action Also remove a no-longer existing file --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 906af9910c..ad48443607 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,8 @@ cmake_minimum_required(VERSION 2.8.12) install(FILES __init__.py - HttpUploadDataStream.py + DiscoverUM3Action.py + DiscoverUM3Action.qml NetworkPrinterOutputDevice.py NetworkPrinterOutputDevicePlugin.py LICENSE From 676182cd54fd409d3c4c20797e15eec8906ac6a4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Jul 2016 10:52:31 +0200 Subject: [PATCH 112/438] Fixed authentication timeout CURA-1916 --- NetworkPrinterOutputDevice.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index d082206a67..25eefadf9f 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -157,13 +157,22 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message.hide() authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) authentication_succeeded_message.show() + + # Stop waiting for a response + self._authentication_timer.stop() + self._authentication_counter = 0 + # Once we are authenticated we need to send all material profiles. - # self.sendMaterialProfiles() elif auth_state == AuthState.AuthenticationDenied: self._authentication_requested_message.hide() authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed. This can be either due to a timeout or the printer refused the request.")) authentication_failed_message.show() + + # Stop waiting for a response + self._authentication_timer.stop() + self._authentication_counter = 0 + self._authentication_state = auth_state ## Request data from the connected device. From f9f83981bb8ac9a055f0466be049d529403db1cf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Jul 2016 14:06:50 +0200 Subject: [PATCH 113/438] Changed print with wifi to print over network CURA-1855 --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 25eefadf9f..cef661d1a2 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -54,8 +54,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" self.setName(key) - self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with WIFI")) - self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with WIFI")) + self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print over network")) + self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setIconName("print") # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly From 6e84d39cae0c2fc6af4b4efba02e3918c754814c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Jul 2016 14:14:22 +0200 Subject: [PATCH 114/438] Improved error handling of authentication --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index cef661d1a2..cb1a7789ca 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -427,7 +427,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._requestAuthentication() elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 403: pass - else: + elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self.setAuthenticationState(AuthState.Authenticated) global_container_stack = Application.getInstance().getGlobalContainerStack() ## Save authentication details. @@ -441,6 +441,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): else: global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id) Logger.log("i", "Authentication succeeded") + else: # Got a response that we didn't expect, so something went wrong. + Logger.log("w", "While trying to authenticate, we got an unexpected response: %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) + self.setAuthenticationState(AuthState.NotAuthenticated) + elif "auth/check" in reply.url().toString(): # Check if we are authenticated (user can refuse this!) data = json.loads(bytes(reply.readAll()).decode("utf-8")) if data.get("message", "") == "authorized": From 53a9f5f622546ba23392e6b3ecc25e83f15aaedd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Jul 2016 14:15:30 +0200 Subject: [PATCH 115/438] Added log message to authentication timeout --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index cb1a7789ca..27a11916d4 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -110,6 +110,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter * 100) if self._authentication_counter > self._max_authentication_counter: self._authentication_timer.stop() + Logger.log("i", "Authentication timer ended. Setting authentication to denied") self.setAuthenticationState(AuthState.AuthenticationDenied) def _onAuthenticationRequired(self, reply, authenticator): From 097d4c9e6b3028fa7f3c12f965ebb7f77da1ed73 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Jul 2016 15:03:47 +0200 Subject: [PATCH 116/438] Added more logging --- NetworkPrinterOutputDevice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 27a11916d4..065ac7c4f0 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -388,6 +388,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Handler for all requests that have finished. def _onFinished(self, reply): + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: @@ -397,6 +398,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._spliceJSONData() else: + Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) pass # TODO: Handle errors elif "print_job" in reply.url().toString(): # Status update from print_job: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: @@ -416,6 +418,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName("") + else: + Logger.log("w", "We got an unexpected status (%s) while requesting print job state", status_code) elif "snapshot" in reply.url().toString(): # Status update from image: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) From 479a155b39c4cd54a355b5ab041921e83fa110c2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Jul 2016 15:05:02 +0200 Subject: [PATCH 117/438] Code cleanup --- NetworkPrinterOutputDevice.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 065ac7c4f0..c7750cd538 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -391,7 +391,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + if status_code == 200: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8")) @@ -401,7 +401,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) pass # TODO: Handle errors elif "print_job" in reply.url().toString(): # Status update from print_job: - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + if status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"] ## If progress is 0 add a bit so another print can't be sent. @@ -412,7 +412,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) self.setJobName(json_data["name"]) - elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 404: + elif status_code == 404: self.setProgress(0) # No print job found, so there can't be progress or other data. self._updateJobState("") self.setTimeElapsed(0) @@ -421,18 +421,18 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): else: Logger.log("w", "We got an unexpected status (%s) while requesting print job state", status_code) elif "snapshot" in reply.url().toString(): # Status update from image: - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + if status_code == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() elif "auth/verify" in reply.url().toString(): # Answer when requesting authentication - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 401: + if status_code == 401: if self._authentication_state != AuthState.AuthenticationRequested: # Only request a new authentication when we have not already done so. Logger.log("i", "Not authenticated. Attempting to request authentication") self._requestAuthentication() - elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 403: + elif status_code == 403: pass - elif reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + elif status_code == 200: self.setAuthenticationState(AuthState.Authenticated) global_container_stack = Application.getInstance().getGlobalContainerStack() ## Save authentication details. @@ -478,8 +478,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() elif reply.operation() == QNetworkAccessManager.PutOperation: - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 204: - pass # Request was sucesfull! + if status_code == 204: + pass # Request was successful! else: Logger.log("d", "Something went wrong when trying to update data of API (%s). Message: %s Statuscode: %s", reply.url().toString(), reply.readAll(), reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) else: From 01f051b82a7a51ea1c04c01df36531ea00129664 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Jul 2016 15:10:57 +0200 Subject: [PATCH 118/438] Timeouts are now logged CURA-1851 --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index c7750cd538..b22d36f41d 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -9,7 +9,7 @@ import UM.Settings from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState -from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager +from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot from PyQt5.QtGui import QImage @@ -388,6 +388,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Handler for all requests that have finished. def _onFinished(self, reply): + if reply.error() == QNetworkReply.TimeoutError: + Logger.log("w", "Received a timeout on a request to the printer") + return + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. From 0df2543f1a1e84baf768c9efc9a8e49696654fff Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 21 Jul 2016 11:58:35 +0200 Subject: [PATCH 119/438] Fixed timeout implementation Turned out that QT timeout wasn't triggred at all, so we're now keeping track of this with our own timer CURA-1851 --- NetworkPrinterOutputDevice.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index b22d36f41d..3001c1b41a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -16,6 +16,8 @@ from PyQt5.QtGui import QImage import json import os +from time import time + i18n_catalog = i18nCatalog("cura") from enum import IntEnum @@ -104,6 +106,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_image = QImage() self._material_post_objects = {} + self._connection_state_before_timeout = None + + self._last_response_time = time() + self._timeout_time = 10 def _onAuthenticationTimer(self): self._authentication_counter += 1 @@ -178,6 +184,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Request data from the connected device. def _update(self): + # Check that we aren't in a timeout state + if self._last_response_time and not self._connection_state_before_timeout: + if time() - self._last_response_time > self._timeout_time: + # Go into timeout state. + Logger.log("d", "We did not recieve a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) + self._connection_state_before_timeout = self._connection_state + self.setConnectionState(ConnectionState.error) + if self._authentication_state == AuthState.NotAuthenticated: self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. elif self._authentication_state == AuthState.AuthenticationRequested: @@ -390,8 +404,20 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _onFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the printer") + self._connection_state_before_timeout = self._connection_state + self.setConnectionState(ConnectionState.error) return + if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. + Logger.log("d", "We got a response from the server after %s of silence", time() - self._last_response_time ) + self.setConnectionState(self._connection_state_before_timeout) + self._connection_state_before_timeout = None + + if reply.error() == QNetworkReply.NoError: + self._last_response_time = time() + else: + return # Error in the reply, drop it. + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. From e584275f7a66d45604f6aa8abced898349b56fbb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 25 Jul 2016 16:31:18 +0200 Subject: [PATCH 120/438] No longer drop messages that have an error state. I overlooked that authentication required is also an error state Fixes CURA-1975 --- NetworkPrinterOutputDevice.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 3001c1b41a..e593b0959b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -415,8 +415,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if reply.error() == QNetworkReply.NoError: self._last_response_time = time() - else: - return # Error in the reply, drop it. status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: From ea022204adee92155fc963884ec38a48411fb4fc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 27 Jul 2016 13:10:57 +0200 Subject: [PATCH 121/438] Decreased timeout time to 5 sec --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e593b0959b..f91eaed05c 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -109,7 +109,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_state_before_timeout = None self._last_response_time = time() - self._timeout_time = 10 + self._response_timeout_time = 5 def _onAuthenticationTimer(self): self._authentication_counter += 1 @@ -186,7 +186,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _update(self): # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: - if time() - self._last_response_time > self._timeout_time: + if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. Logger.log("d", "We did not recieve a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state From 3c2836f3f5d6a09f4ed3aa76fb0a7458a481958d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 27 Jul 2016 13:43:27 +0200 Subject: [PATCH 122/438] Connecting with a different machine resets the authentication data --- DiscoverUM3Action.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 27c5ef6638..df92e36cf8 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -38,8 +38,12 @@ class DiscoverUM3Action(MachineAction): def setKey(self, key): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: - if "um_network_key" in global_container_stack.getMetaData(): + meta_data = global_container_stack.getMetaData() + if "um_network_key" in meta_data: global_container_stack.setMetaDataEntry("um_network_key", key) + # Delete old authentication data. + global_container_stack.removeMetaDataEntry("network_authentication_id") + global_container_stack.removeMetaDataEntry("network_authentication_key") else: global_container_stack.addMetaDataEntry("um_network_key", key) From fd36f09b29cfc3ac000070f14d9b3e5aba2906cf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 27 Jul 2016 14:14:43 +0200 Subject: [PATCH 123/438] "none" as state is now sent as empty string --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f91eaed05c..11aa6992ef 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -436,7 +436,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if progress == 0: progress += 0.001 self.setProgress(progress * 100) - self._updateJobState(json_data["state"]) + + state = json_data["state"] + if state == "none": + state = "" + self._updateJobState(state) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) self.setJobName(json_data["name"]) From 050e81053f27eac4d0e31675ec6e2aba4ceb6b24 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 27 Jul 2016 15:36:08 +0200 Subject: [PATCH 124/438] Double progress bar no longer occurs --- NetworkPrinterOutputDevice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 11aa6992ef..109d3dd195 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -506,9 +506,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif "materials" in reply.url().toString(): # Remove cached post request items. del self._material_post_objects[id(reply)] - else: + elif "print_job" in reply.url().toString(): reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() + elif reply.operation() == QNetworkAccessManager.PutOperation: if status_code == 204: pass # Request was successful! @@ -519,6 +520,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: - self._progress_message.setProgress(bytes_sent / bytes_total * 100) + new_progress = bytes_sent / bytes_total * 100 + if new_progress > self._progress_message.getProgress(): + self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) \ No newline at end of file From 136755758e564f8eccc0b4ea0aa098252d05fbad Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 27 Jul 2016 16:41:06 +0200 Subject: [PATCH 125/438] Implement ServiceStateChange.Removed (bonjour undiscovery) Contributes to CURA-1851 --- DiscoverUM3Action.py | 13 +++++++------ NetworkPrinterOutputDevice.py | 6 ++++++ NetworkPrinterOutputDevicePlugin.py | 13 ++++++++++--- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index df92e36cf8..9ffb28c74d 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -14,19 +14,20 @@ class DiscoverUM3Action(MachineAction): self._network_plugin = None - printerDetected = pyqtSignal() + printersChanged = pyqtSignal() @pyqtSlot() def startDiscovery(self): if not self._network_plugin: self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("JediWifiPrintingPlugin") - self._network_plugin.addPrinterSignal.connect(self._onPrinterAdded) - self.printerDetected.emit() + self._network_plugin.addPrinterSignal.connect(self._onPrinterDiscoveryChanged) + self._network_plugin.removePrinterSignal.connect(self._onPrinterDiscoveryChanged) + self.printersChanged.emit() - def _onPrinterAdded(self, *args): - self.printerDetected.emit() + def _onPrinterDiscoveryChanged(self, *args): + self.printersChanged.emit() - @pyqtProperty("QVariantList", notify = printerDetected) + @pyqtProperty("QVariantList", notify = printersChanged) def foundDevices(self): if self._network_plugin: printers = self._network_plugin.getPrinters() diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 109d3dd195..71c8bf94f9 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -233,6 +233,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._updateHeadPosition(head_x, head_y, head_z) def close(self): + self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() @@ -279,6 +280,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.start() self._camera_timer.start() + ## Stop requesting data from printer + def disconnect(self): + Logger.log("d", "Connection with printer %s with ip %s stopped", self._key, self._address) + self.close() + newImage = pyqtSignal() @pyqtProperty(QUrl, notify = newImage) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 4055b59a1a..5b5a04d9f3 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -19,9 +19,11 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addPrinterSignal.connect(self.addPrinter) + self.removePrinterSignal.connect(self.removePrinter) Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) addPrinterSignal = Signal() + removePrinterSignal = Signal() ## Start looking for devices on network. def start(self): @@ -56,6 +58,13 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + def removePrinter(self, name): + printer = self._printers.pop(name, None) + if printer: + if printer.isConnected(): + printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged) + printer.disconnect() + ## Handler for when the connection state of one of the detected printers changes def _onPrinterConnectionStateChanged(self, key): if self._printers[key].isConnected(): @@ -73,6 +82,4 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.addPrinterSignal.emit(str(name), address, info.properties) elif state_change == ServiceStateChange.Removed: - pass - # TODO; This isn't testable right now. We need to also decide how to handle - # \ No newline at end of file + self.removePrinterSignal.emit(str(name)) \ No newline at end of file From 113129eaca8a421b838c3edb822210ea7b47ac05 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 28 Jul 2016 16:15:31 +0200 Subject: [PATCH 126/438] Fix typo --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 71c8bf94f9..94cc689940 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -188,7 +188,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. - Logger.log("d", "We did not recieve a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) + Logger.log("d", "We did not receive a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) From f0521291716b1c12164bc438fd0021b4d1a8d484 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 28 Jul 2016 18:09:26 +0200 Subject: [PATCH 127/438] Show a message to the user when the connection with the printer is lost CURA-1851 --- NetworkPrinterOutputDevice.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 94cc689940..28a25de4f8 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -77,6 +77,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message = None self._error_message = None + self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval(2000) # TODO; Add preference for update interval @@ -190,12 +191,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Go into timeout state. Logger.log("d", "We did not receive a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state + self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost.Check your network-connections.")) + self._connection_message.show() self.setConnectionState(ConnectionState.error) if self._authentication_state == AuthState.NotAuthenticated: self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. elif self._authentication_state == AuthState.AuthenticationRequested: self._checkAuthentication() # We requested authentication at some point. Check if we got permission. + ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") printer_request = QNetworkRequest(url) @@ -423,6 +427,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + if not status_code: + # Received no or empty reply + return + if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. if status_code == 200: @@ -431,6 +439,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8")) self._spliceJSONData() + + # Hide connection error message if the connection was restored + if self._connection_message: + self._connection_message.hide() + self._connection_message = None else: Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) pass # TODO: Handle errors @@ -515,7 +528,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif "print_job" in reply.url().toString(): reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() - + elif reply.operation() == QNetworkAccessManager.PutOperation: if status_code == 204: pass # Request was successful! From b08bdb206bc51beadd56e0bdd4f4a45ed523206b Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 28 Jul 2016 18:48:15 +0200 Subject: [PATCH 128/438] Added logging for bonjour discovery/undiscovery CURA-1851 --- NetworkPrinterOutputDevicePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 5b5a04d9f3..a36f7ea9eb 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -2,6 +2,7 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from . import NetworkPrinterOutputDevice from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange +from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.Application import Application @@ -75,6 +76,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): ## Handler for zeroConf detection def _onServiceChanged(self, zeroconf, service_type, name, state_change): if state_change == ServiceStateChange.Added: + Logger.log("d", "Bonjour service added: %s" % name) info = zeroconf.get_service_info(service_type, name) if info: if info.properties.get(b"type", None) == b'printer': @@ -82,4 +84,5 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.addPrinterSignal.emit(str(name), address, info.properties) elif state_change == ServiceStateChange.Removed: + Logger.log("d", "Bonjour service removed: %s" % name) self.removePrinterSignal.emit(str(name)) \ No newline at end of file From 6d94d3e1d904226cc2583c61cf3eda184f8e8e97 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 29 Jul 2016 11:25:45 +0200 Subject: [PATCH 129/438] Remove Ok/Cancel buttons to better fit in wizard or action dialog CURA-2019 --- DiscoverUM3Action.qml | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index d74a39ade7..3d57bb84d1 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -142,29 +142,19 @@ Cura.MachineAction text: base.selectedPrinter ? base.selectedPrinter.ipAddress : "" } } + + Button + { + text: catalog.i18nc("@action:button", "Ok") + enabled: base.selectedPrinter + onClicked: + { + manager.setKey(base.selectedPrinter.getKey()) + completed() + } + } + } } } - Button - { - text: catalog.i18nc("@action:button", "Ok") - anchors.right: cancelButton.left - anchors.bottom: parent.bottom - onClicked: - { - manager.setKey(base.selectedPrinter.getKey()) - completed() - } - } - Button - { - id: cancelButton - text: catalog.i18nc("@action:button", "Cancel") - anchors.right: discoverUM3Action.right - anchors.bottom: parent.bottom - onClicked: - { - completed() - } - } } \ No newline at end of file From 14624e7acba1ea4338d591e0cb48fa48bda138f2 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 29 Jul 2016 12:39:31 +0200 Subject: [PATCH 130/438] Change "Ok" button caption to "Connect" CURA-2019 --- DiscoverUM3Action.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 3d57bb84d1..ff8461ce21 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -145,7 +145,7 @@ Cura.MachineAction Button { - text: catalog.i18nc("@action:button", "Ok") + text: catalog.i18nc("@action:button", "Connect") enabled: base.selectedPrinter onClicked: { From 80f5ad3b913131d99f708cca0ca27be041efc7ab Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 29 Jul 2016 17:45:52 +0200 Subject: [PATCH 131/438] Codestyle & documentation --- NetworkPrinterOutputDevice.py | 36 +++++++++++------------------------ 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 28a25de4f8..096f6a3193 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -77,7 +77,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message = None self._error_message = None - self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval(2000) # TODO; Add preference for update interval @@ -111,6 +110,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() self._response_timeout_time = 5 + self._not_authenticated_message = None def _onAuthenticationTimer(self): self._authentication_counter += 1 @@ -189,16 +189,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. - Logger.log("d", "We did not receive a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) + Logger.log("d", "We did not recieve a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state - self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost.Check your network-connections.")) - self._connection_message.show() self.setConnectionState(ConnectionState.error) if self._authentication_state == AuthState.NotAuthenticated: - self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. + self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. elif self._authentication_state == AuthState.AuthenticationRequested: - self._checkAuthentication() # We requested authentication at some point. Check if we got permission. + self._checkAuthentication() # We requested authentication at some point. Check if we got permission. ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") @@ -237,7 +235,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._updateHeadPosition(head_x, head_y, head_z) def close(self): - self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() @@ -284,16 +281,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.start() self._camera_timer.start() - ## Stop requesting data from printer - def disconnect(self): - Logger.log("d", "Connection with printer %s with ip %s stopped", self._key, self._address) - self.close() - newImage = pyqtSignal() @pyqtProperty(QUrl, notify = newImage) def cameraImage(self): self._camera_image_id += 1 + # There is an image provider that is called "camera". In order to ensure that the image qml object, that + # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl + # as new (instead of relying on cached version and thus forces an update. temp = "image://camera/" + str(self._camera_image_id) return QUrl(temp, QUrl.TolerantMode) @@ -310,7 +305,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Convenience function to get the username from the OS. # The code was copied from the getpass module, as we try to use as little dependencies as possible. def _getUserName(self): - for name in ('LOGNAME', 'USER', 'LNAME', 'USERNAME'): + for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): user = os.environ.get(name) if user: return user @@ -418,8 +413,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setConnectionState(ConnectionState.error) return - if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. - Logger.log("d", "We got a response from the server after %s of silence", time() - self._last_response_time ) + if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. + Logger.log("d", "We got a response from the server after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None @@ -427,10 +422,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if not status_code: - # Received no or empty reply - return - if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. if status_code == 200: @@ -439,11 +430,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8")) self._spliceJSONData() - - # Hide connection error message if the connection was restored - if self._connection_message: - self._connection_message.hide() - self._connection_message = None else: Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) pass # TODO: Handle errors @@ -543,4 +529,4 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if new_progress > self._progress_message.getProgress(): self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: - self._progress_message.setProgress(0) \ No newline at end of file + self._progress_message.setProgress(0) From 6c0168c3ea1baf09a63a0b2cb5a659cc74290278 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 1 Aug 2016 15:23:23 +0200 Subject: [PATCH 132/438] Connect to selected printer when confirming the dialog CURA-2019 --- DiscoverUM3Action.qml | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index ff8461ce21..672b870c6b 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -11,6 +11,23 @@ Cura.MachineAction id: base anchors.fill: parent; property var selectedPrinter: null + + Connections + { + target: dialog ? dialog : null + ignoreUnknownSignals: true + onNextClicked: connectToPrinter() + } + + function connectToPrinter() + { + if(base.selectedPrinter) + { + manager.setKey(base.selectedPrinter.getKey()) + completed() + } + } + Column { anchors.fill: parent; @@ -57,7 +74,7 @@ Cura.MachineAction id: listview model: manager.foundDevices width: parent.width - currentIndex: activeIndex + currentIndex: -1 onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] Component.onCompleted: manager.startDiscovery() delegate: Rectangle @@ -147,13 +164,8 @@ Cura.MachineAction { text: catalog.i18nc("@action:button", "Connect") enabled: base.selectedPrinter - onClicked: - { - manager.setKey(base.selectedPrinter.getKey()) - completed() - } + onClicked: connectToPrinter() } - } } } From c9daaf15212970e0565111f4f5efe1b127d0cbf4 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 1 Aug 2016 15:30:38 +0200 Subject: [PATCH 133/438] Reapply changes that were lost in 80f5ad3 --- NetworkPrinterOutputDevice.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 096f6a3193..99725e5a54 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -77,6 +77,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message = None self._error_message = None + self._connection_message = None self._update_timer = QTimer() self._update_timer.setInterval(2000) # TODO; Add preference for update interval @@ -189,8 +190,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. - Logger.log("d", "We did not recieve a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) + Logger.log("d", "We did not receive a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state + self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost.Check your network-connections.")) + self._connection_message.show() self.setConnectionState(ConnectionState.error) if self._authentication_state == AuthState.NotAuthenticated: @@ -235,6 +238,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._updateHeadPosition(head_x, head_y, head_z) def close(self): + self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() @@ -281,6 +285,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.start() self._camera_timer.start() + ## Stop requesting data from printer + def disconnect(self): + Logger.log("d", "Connection with printer %s with ip %s stopped", self._key, self._address) + self.close() + newImage = pyqtSignal() @pyqtProperty(QUrl, notify = newImage) @@ -422,6 +431,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + if not status_code: + # Received no or empty reply + return + if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from printer. if status_code == 200: @@ -430,6 +443,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8")) self._spliceJSONData() + + # Hide connection error message if the connection was restored + if self._connection_message: + self._connection_message.hide() + self._connection_message = None else: Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) pass # TODO: Handle errors From d7f3b734203341cb3bc131f6e4c7aaecc43dc9c2 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 2 Aug 2016 10:31:49 +0200 Subject: [PATCH 134/438] Prevent infinite loop when adding a Jedi printer CURA-2019 --- DiscoverUM3Action.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 672b870c6b..6ccb078bbd 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -11,6 +11,7 @@ Cura.MachineAction id: base anchors.fill: parent; property var selectedPrinter: null + property var connectingWithPrinter: null Connections { @@ -23,8 +24,13 @@ Cura.MachineAction { if(base.selectedPrinter) { - manager.setKey(base.selectedPrinter.getKey()) - completed() + var printerKey = base.selectedPrinter.getKey() + if(connectingWithPrinter != printerKey) { + // prevent an infinite loop + connectingWithPrinter = printerKey; + manager.setKey(printerKey); + completed(); + } } } From bb8e74eb48299ae0eedf5710bfec07e685d628ed Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 2 Aug 2016 11:37:04 +0200 Subject: [PATCH 135/438] Fix connecting to the same printer twice CURA-2019 --- DiscoverUM3Action.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 6ccb078bbd..624b36b9bd 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -11,7 +11,7 @@ Cura.MachineAction id: base anchors.fill: parent; property var selectedPrinter: null - property var connectingWithPrinter: null + property var connectingToPrinter: null Connections { @@ -25,11 +25,14 @@ Cura.MachineAction if(base.selectedPrinter) { var printerKey = base.selectedPrinter.getKey() - if(connectingWithPrinter != printerKey) { + if(connectingToPrinter != printerKey) { // prevent an infinite loop - connectingWithPrinter = printerKey; + connectingToPrinter = printerKey; manager.setKey(printerKey); completed(); + } else { + // reset, so we can connect to the same printer again if needed + connectingToPrinter = null; } } } From 292826f2236e4fe32c4ef514293ca5044622c03e Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 2 Aug 2016 14:18:01 +0200 Subject: [PATCH 136/438] Fix typo Missing space. --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 99725e5a54..ec0cbeca66 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -192,7 +192,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Go into timeout state. Logger.log("d", "We did not receive a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state - self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost.Check your network-connections.")) + self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your network-connections.")) self._connection_message.show() self.setConnectionState(ConnectionState.error) From 805c513034d592cfca30c8a078702a2e423c9371 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 2 Aug 2016 16:23:33 +0200 Subject: [PATCH 137/438] Code cleanup CURA-2019 --- DiscoverUM3Action.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 624b36b9bd..d7992a1f5f 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -30,9 +30,6 @@ Cura.MachineAction connectingToPrinter = printerKey; manager.setKey(printerKey); completed(); - } else { - // reset, so we can connect to the same printer again if needed - connectingToPrinter = null; } } } From 10be9e66748bb8aa3628313f5b9606fad12dd0e9 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 3 Aug 2016 17:04:50 +0200 Subject: [PATCH 138/438] Fix an warning when a printer has a timeout before it has been fully discovered CURA-1851 --- NetworkPrinterOutputDevicePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index a36f7ea9eb..8db0c4a783 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -68,6 +68,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): ## Handler for when the connection state of one of the detected printers changes def _onPrinterConnectionStateChanged(self, key): + if key not in self._printers: + return if self._printers[key].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._printers[key]) else: From 0b1c4ea5519e1cabcba9accc2012f8034dca3e70 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 3 Aug 2016 17:05:20 +0200 Subject: [PATCH 139/438] Make sure the output device gets selected above local file output --- NetworkPrinterOutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index ec0cbeca66..37542a74e6 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -37,6 +37,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._key = key self._properties = properties # Properties dict as provided by zero conf + self.setPriority(2) # Make sure the output device gets selected above local file output + self._gcode = None # This holds the full JSON file that was received from the last request. From c4850a6ff22ce067ead3622c5782e37ac39e94d4 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 4 Aug 2016 12:57:13 +0200 Subject: [PATCH 140/438] Show error state after aborting a print CURA-1990 --- NetworkPrinterOutputDevice.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 37542a74e6..e64a86f9f2 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -37,9 +37,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._key = key self._properties = properties # Properties dict as provided by zero conf - self.setPriority(2) # Make sure the output device gets selected above local file output - self._gcode = None + self._print_finished = True # _print_finsihed == False means we're halfway in a print # This holds the full JSON file that was received from the last request. self._json_printer_state = None @@ -57,6 +56,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._api_version = "1" self._api_prefix = "/api/v" + self._api_version + "/" + self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print over network")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) @@ -254,6 +254,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def requestWrite(self, node, file_name = None, filter_by_machine = False): Application.getInstance().showPrintMonitor.emit(True) + self._print_finished = True self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") # TODO: Implement all checks. @@ -460,11 +461,27 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## If progress is 0 add a bit so another print can't be sent. if progress == 0: progress += 0.001 + elif progress == 1: + self._print_finished = True + else: + self._print_finished = False self.setProgress(progress * 100) state = json_data["state"] - if state == "none": - state = "" + + # There is a short period after aborting or finishing a print where the printer + # reports a "none" state (but the printer is not ready to receive a print) + # If this happens before the print has reached progress == 1, the print has + # been aborted. + if state == "none" or state == "": + if self._print_finished: + state = "printing" + else: + state = "error" + if state == "wait_cleanup" and not self._print_finished: + # Keep showing the "aborted" error state until after the buildplate has been cleaned + state = "error" + self._updateJobState(state) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) From 31861d82dcc81b26986750688b226987e023ce63 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 4 Aug 2016 13:27:42 +0200 Subject: [PATCH 141/438] Show message while aborting CURA-1990 --- NetworkPrinterOutputDevice.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e64a86f9f2..eda865f7ca 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -477,9 +477,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._print_finished: state = "printing" else: + self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Aborting print...")) state = "error" if state == "wait_cleanup" and not self._print_finished: # Keep showing the "aborted" error state until after the buildplate has been cleaned + self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Print aborted. Please check the printer")) state = "error" self._updateJobState(state) @@ -489,6 +491,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif status_code == 404: self.setProgress(0) # No print job found, so there can't be progress or other data. self._updateJobState("") + self.setErrorText("") self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName("") From ef36c70d83cfbdbbe751516c92b960a92a9326f4 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 4 Aug 2016 14:23:57 +0200 Subject: [PATCH 142/438] Check if the printer is ready to receive *before* doing work. This has very little to do with CURA-1990 --- NetworkPrinterOutputDevice.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index eda865f7ca..4fc3a96276 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -253,6 +253,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_timer.stop() def requestWrite(self, node, file_name = None, filter_by_machine = False): + if self._progress != 0: + self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) + self._error_message.show() + return + elif self._authentication_state != AuthState.Authenticated: + self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", + "Not authenticated to print with this machine. Unable to start a new job.")) + self._not_authenticated_message.show() + return + Application.getInstance().showPrintMonitor.emit(True) self._print_finished = True self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") @@ -327,15 +337,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # This function can fail to actually start a print due to not being authenticated or another print already # being in progress. def startPrint(self): - if self._progress != 0: - self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) - self._error_message.show() - return - elif self._authentication_state != AuthState.Authenticated: - self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", - "Not authenticated to print with this machine. Unable to start a new job.")) - self._not_authenticated_message.show() - return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() From d3333540de1307cca0b3113ef38978adc06e3799 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 4 Aug 2016 18:16:05 +0200 Subject: [PATCH 143/438] Show printer in "indeterminate" state when we are not authenticated CURA-1851 --- NetworkPrinterOutputDevice.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 4fc3a96276..35425643db 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -162,9 +162,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # \param auth_state \type{AuthState} Enum value representing the new auth state def setAuthenticationState(self, auth_state): if auth_state == AuthState.AuthenticationRequested: + self.setAcceptsCommands(False) self._authentication_requested_message.show() self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: + self.setAcceptsCommands(True) self._authentication_requested_message.hide() authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) authentication_succeeded_message.show() @@ -176,6 +178,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Once we are authenticated we need to send all material profiles. self.sendMaterialProfiles() elif auth_state == AuthState.AuthenticationDenied: + self.setAcceptsCommands(False) self._authentication_requested_message.hide() authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed. This can be either due to a timeout or the printer refused the request.")) authentication_failed_message.show() From 70a93ac0a26973142ab6832b63a65bcd48d268c4 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 10:27:19 +0200 Subject: [PATCH 144/438] Only associate to a Jedi if the MachineAction is currently shown CURA-2041 --- DiscoverUM3Action.qml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index d7992a1f5f..e679413770 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -17,7 +17,14 @@ Cura.MachineAction { target: dialog ? dialog : null ignoreUnknownSignals: true - onNextClicked: connectToPrinter() + onNextClicked: + { + // Connect to the printer if the MachineAction is currently shown + if(base.parent == dialog) + { + connectToPrinter(); + } + } } function connectToPrinter() From a288ab321fdf6987b742c44646774be32d8c3e3b Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 9 Aug 2016 11:19:39 +0200 Subject: [PATCH 145/438] Display an error message when "Print over network" if printer not idle. CURA-1986 --- NetworkPrinterOutputDevice.py | 54 +++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 35425643db..758fd9216a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -40,8 +40,28 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._gcode = None self._print_finished = True # _print_finsihed == False means we're halfway in a print - # This holds the full JSON file that was received from the last request. - self._json_printer_state = None + # This holds the full JSON file that was received from the last request. + # The JSON looks like: + # {'led': {'saturation': 0.0, 'brightness': 100.0, 'hue': 0.0}, + # 'beep': {}, 'network': {'wifi_networks': [], + # 'ethernet': {'connected': True, 'enabled': True}, + # 'wifi': {'ssid': 'xxxx', 'connected': False, 'enabled': False}}, + # 'diagnostics': {}, + # 'bed': {'temperature': {'target': 60.0, 'current': 44.4}}, + # 'heads': [{'max_speed': {'z': 40.0, 'y': 300.0, 'x': 300.0}, + # 'position': {'z': 20.0, 'y': 6.0, 'x': 180.0}, + # 'fan': 0.0, + # 'jerk': {'z': 0.4, 'y': 20.0, 'x': 20.0}, + # 'extruders': [ + # {'feeder': {'max_speed': 45.0, 'jerk': 5.0, 'acceleration': 3000.0}, + # 'active_material': {'GUID': 'xxxxxxx', 'length_remaining': -1.0}, + # 'hotend': {'temperature': {'target': 0.0, 'current': 22.8}, 'id': 'AA 0.4'}}, + # {'feeder': {'max_speed': 45.0, 'jerk': 5.0, 'acceleration': 3000.0}, + # 'active_material': {'GUID': 'xxxx', 'length_remaining': -1.0}, + # 'hotend': {'temperature': {'target': 0.0, 'current': 22.8}, 'id': 'BB 0.4'}}], + # 'acceleration': 3000.0}], + # 'status': 'printing'} + self._json_printer_state = {} ## Todo: Hardcoded value now; we should probably read this from the machine file. ## It's okay to leave this for now, as this plugin is um3 only (and has 2 extruders by definition) @@ -73,7 +93,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_multi_part = None self._post_part = None - self._material_multi_part = None self._material_part = None @@ -195,7 +214,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. - Logger.log("d", "We did not receive a response for %s seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) + Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your network-connections.")) self._connection_message.show() @@ -260,6 +279,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) self._error_message.show() return + if self._json_printer_state["status"] != "idle": + self._error_message = Message( + i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is not idle. Current printer status is %s.") % self._json_printer_state["status"]) + self._error_message.show() + return elif self._authentication_state != AuthState.Authenticated: self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", "Not authenticated to print with this machine. Unable to start a new job.")) @@ -430,7 +454,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. - Logger.log("d", "We got a response from the server after %s of silence", time() - self._last_response_time) + Logger.log("d", "We got a response from the server after %0.1f of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None @@ -441,14 +465,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if not status_code: # Received no or empty reply return + reply_url = reply.url().toString() if reply.operation() == QNetworkAccessManager.GetOperation: - if "printer" in reply.url().toString(): # Status update from printer. + if "printer" in reply_url: # Status update from printer. if status_code == 200: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) self._json_printer_state = json.loads(bytes(reply.readAll()).decode("utf-8")) - self._spliceJSONData() # Hide connection error message if the connection was restored @@ -458,7 +482,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): else: Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) pass # TODO: Handle errors - elif "print_job" in reply.url().toString(): # Status update from print_job: + elif "print_job" in reply_url: # Status update from print_job: if status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"] @@ -501,11 +525,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setJobName("") else: Logger.log("w", "We got an unexpected status (%s) while requesting print job state", status_code) - elif "snapshot" in reply.url().toString(): # Status update from image: + elif "snapshot" in reply_url: # Status update from image: if status_code == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() - elif "auth/verify" in reply.url().toString(): # Answer when requesting authentication + elif "auth/verify" in reply_url: # Answer when requesting authentication if status_code == 401: if self._authentication_state != AuthState.AuthenticationRequested: # Only request a new authentication when we have not already done so. @@ -531,7 +555,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("w", "While trying to authenticate, we got an unexpected response: %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) self.setAuthenticationState(AuthState.NotAuthenticated) - elif "auth/check" in reply.url().toString(): # Check if we are authenticated (user can refuse this!) + elif "auth/check" in reply_url: # Check if we are authenticated (user can refuse this!) data = json.loads(bytes(reply.readAll()).decode("utf-8")) if data.get("message", "") == "authorized": Logger.log("i", "Authentication was approved") @@ -543,7 +567,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass elif reply.operation() == QNetworkAccessManager.PostOperation: - if "/auth/request" in reply.url().toString(): + if "/auth/request" in reply_url: # We got a response to requesting authentication. data = json.loads(bytes(reply.readAll()).decode("utf-8")) self._authentication_key = data["key"] @@ -552,10 +576,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check if the authentication is accepted. self._checkAuthentication() - elif "materials" in reply.url().toString(): + elif "materials" in reply_url: # Remove cached post request items. del self._material_post_objects[id(reply)] - elif "print_job" in reply.url().toString(): + elif "print_job" in reply_url: reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() @@ -563,7 +587,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if status_code == 204: pass # Request was successful! else: - Logger.log("d", "Something went wrong when trying to update data of API (%s). Message: %s Statuscode: %s", reply.url().toString(), reply.readAll(), reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) + Logger.log("d", "Something went wrong when trying to update data of API (%s). Message: %s Statuscode: %s", reply_url, reply.readAll(), status_code) else: Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) From 3a0b9ccafe94724c6ddbeef85a68a0e6d0f8a4b6 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 9 Aug 2016 11:22:24 +0200 Subject: [PATCH 146/438] Typo CURA-1986 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 758fd9216a..22b14e3c23 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -214,7 +214,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. - Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accesible.", time() - self._last_response_time) + Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accessible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your network-connections.")) self._connection_message.show() From a0535a7aaf783e2307a7b39d9c6230344455a31f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 11:22:26 +0200 Subject: [PATCH 147/438] Add some logging to debug discovery issues CURA-2035 --- NetworkPrinterOutputDevicePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 8db0c4a783..b9d8cebadf 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -84,6 +84,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if info.properties.get(b"type", None) == b'printer': address = '.'.join(map(lambda n: str(n), info.address)) self.addPrinterSignal.emit(str(name), address, info.properties) + else: + Logger.log("w", "Could not get information about %s" % name) elif state_change == ServiceStateChange.Removed: Logger.log("d", "Bonjour service removed: %s" % name) From 86a65e495bb407127ef3fa681fbb3baafe8c9078 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 9 Aug 2016 13:10:13 +0200 Subject: [PATCH 148/438] Close now resets authentication & timeout data. CURA-1936 --- NetworkPrinterOutputDevice.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 22b14e3c23..60188921f8 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -266,11 +266,21 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() + + # Reset authentication state self._authentication_requested_message.hide() - if self._error_message: - self._error_message.hide() + self._authentication_state = AuthState.NotAuthenticated self._authentication_counter = 0 self._authentication_timer.stop() + + if self._error_message: + self._error_message.hide() + + # Reset timeout state + self._connection_state_before_timeout = None + self._last_response_time = time() + + # Stop update timers self._update_timer.stop() self._camera_timer.stop() @@ -313,6 +323,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Start requesting data from printer def connect(self): + self.close() # Ensure that previous connection (if any) is killed. self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() From a649bdcb325acd31ea82988427ccbecbc0d5d2e6 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 13:49:47 +0200 Subject: [PATCH 149/438] Speed up Bonjour discovery and make it slightly more robust CURA-2035 --- NetworkPrinterOutputDevicePlugin.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index b9d8cebadf..2ded3b48e7 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -1,11 +1,12 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from . import NetworkPrinterOutputDevice -from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.Application import Application +import time ## This plugin handles the connection detection & creation of output device objects for the UM3 printer. # Zero-Conf is used to detect printers, which are saved in a dict. @@ -79,7 +80,20 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def _onServiceChanged(self, zeroconf, service_type, name, state_change): if state_change == ServiceStateChange.Added: Logger.log("d", "Bonjour service added: %s" % name) - info = zeroconf.get_service_info(service_type, name) + + info = ServiceInfo(service_type, name, properties = {}) + for record in zeroconf.cache.entries_with_name(name.lower()): + info.update_record(zeroconf, time.time(), record) + + for record in zeroconf.cache.entries_with_name(info.server): + info.update_record(zeroconf, time.time(), record) + if info.address: + break + + if not info.address: + Logger.log("d", "Trying to get address of %s", name) + info = zeroconf.get_service_info(service_type, name) + if info: if info.properties.get(b"type", None) == b'printer': address = '.'.join(map(lambda n: str(n), info.address)) From bd6e36f4870cb268557922a45033e424915b3b3c Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 13:54:13 +0200 Subject: [PATCH 150/438] Add documentation CURA-2035 --- NetworkPrinterOutputDevicePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 2ded3b48e7..6fada8364a 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -81,6 +81,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if state_change == ServiceStateChange.Added: Logger.log("d", "Bonjour service added: %s" % name) + # First try getting info from zeroconf cache info = ServiceInfo(service_type, name, properties = {}) for record in zeroconf.cache.entries_with_name(name.lower()): info.update_record(zeroconf, time.time(), record) @@ -90,6 +91,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if info.address: break + # Request more data if info is not complete if not info.address: Logger.log("d", "Trying to get address of %s", name) info = zeroconf.get_service_info(service_type, name) From 7f83c22f741d4902f4cbb48a5f5bb5c725404cc7 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 15:18:57 +0200 Subject: [PATCH 151/438] Add "refresh" button to list of network printers CURA-2035 --- DiscoverUM3Action.py | 3 +++ DiscoverUM3Action.qml | 13 ++++++++++++- NetworkPrinterOutputDevicePlugin.py | 9 +++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 9ffb28c74d..c24f90674f 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -23,6 +23,9 @@ class DiscoverUM3Action(MachineAction): self._network_plugin.addPrinterSignal.connect(self._onPrinterDiscoveryChanged) self._network_plugin.removePrinterSignal.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() + else: + # Restart bonjour discovery + self._network_plugin.startDiscovery() def _onPrinterDiscoveryChanged(self, *args): self.printersChanged.emit() diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index e679413770..2a70457f7c 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -45,6 +45,8 @@ Cura.MachineAction { anchors.fill: parent; id: discoverUM3Action + spacing: UM.Theme.getSize("default_margin").height + SystemPalette { id: palette } UM.I18nCatalog { id: catalog; name:"cura" } Label @@ -61,7 +63,16 @@ Cura.MachineAction id: pageDescription width: parent.width wrapMode: Text.WordWrap - text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. \n\nIf you don't want to connect Cura with your Ultimaker 3 now, you can always use a USB drive to transfer g-code files to your printer.\n\nSelect your Ultimaker 3 from the list below:") + text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your Ultimaker 3, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your Ultimaker 3 from the list below:") + } + + Button + { + id: rediscoverButton + text: catalog.i18nc("@title", "Refresh") + onClicked: manager.startDiscovery() + anchors.right: parent.right + anchors.rightMargin: parent.width * 0.5 } Row diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 6fada8364a..dbb40e4d77 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -29,6 +29,15 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): ## Start looking for devices on network. def start(self): + self.startDiscovery() + + def startDiscovery(self): + if self._browser: + self._browser.cancel() + self._browser = None + self._printers = {} + self._zero_conf.__init__() + self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) ## Stop looking for devices on network. From 7e8eb2044afc1c374d10353a78ea2b774fccdbcf Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 15:19:58 +0200 Subject: [PATCH 152/438] Sort list of discovered printers CURA-2035 --- DiscoverUM3Action.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index c24f90674f..fc895a78ff 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -33,8 +33,9 @@ class DiscoverUM3Action(MachineAction): @pyqtProperty("QVariantList", notify = printersChanged) def foundDevices(self): if self._network_plugin: - printers = self._network_plugin.getPrinters() - return list(printers.values()) + printers = list(self._network_plugin.getPrinters().values()) + printers.sort(key = lambda k: k.name) + return printers else: return [] From 70115208459f606337beeed59c54c068fbfd0682 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 15:54:20 +0200 Subject: [PATCH 153/438] Select the UM3 that we are connected to if it is in the list CURA-2035 --- DiscoverUM3Action.py | 10 ++++++++++ DiscoverUM3Action.qml | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index fc895a78ff..4ca500dc48 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -55,3 +55,13 @@ class DiscoverUM3Action(MachineAction): if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() + + @pyqtSlot(result = str) + def getStoredKey(self): + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack: + meta_data = global_container_stack.getMetaData() + if "um_network_key" in meta_data: + return global_container_stack.getMetaDataEntry("um_network_key") + + return "" \ No newline at end of file diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 2a70457f7c..4dcb5e074d 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -97,6 +97,18 @@ Cura.MachineAction { id: listview model: manager.foundDevices + onModelChanged: + { + var selectedKey = manager.getStoredKey(); + for(var i = 0; i < model.length; i++) { + if(model[i].getKey() == selectedKey) + { + currentIndex = i; + return + } + } + currentIndex = -1; + } width: parent.width currentIndex: -1 onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] @@ -134,6 +146,7 @@ Cura.MachineAction { width: parent.width * 0.5 visible: base.selectedPrinter + spacing: UM.Theme.getSize("default_margin").height Label { width: parent.width From 8fbc85dd558e655b64b3d270500ca499f74fe6bb Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 9 Aug 2016 18:44:14 +0200 Subject: [PATCH 154/438] Add a connection string to the printmonitor CURA-2091 --- NetworkPrinterOutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 60188921f8..feaa71e393 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -182,10 +182,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def setAuthenticationState(self, auth_state): if auth_state == AuthState.AuthenticationRequested: self.setAcceptsCommands(False) + self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0} without access to control the printer.").format(self.name)) self._authentication_requested_message.show() self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: self.setAcceptsCommands(True) + self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}.").format(self.name)) self._authentication_requested_message.hide() authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) authentication_succeeded_message.show() From bd7e8e4c52bbc41eb5306648140b40ff4c97159f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 10 Aug 2016 10:46:02 +0200 Subject: [PATCH 155/438] Progress is now hidden when connection is lost CURA-1851 --- NetworkPrinterOutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index feaa71e393..cfcc5affc3 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -608,6 +608,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 if new_progress > self._progress_message.getProgress(): + self._progress_message.show() # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) + self._progress_message.hide() From 925795401f0411526346f4cb037fe8f45a8f1df3 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Aug 2016 17:36:26 +0200 Subject: [PATCH 156/438] Fix for abort network printer during pre_print. CURA-2093 --- NetworkPrinterOutputDevice.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 60188921f8..e721721474 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -134,6 +134,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._response_timeout_time = 5 self._not_authenticated_message = None + self._last_command = "" + def _onAuthenticationTimer(self): self._authentication_counter += 1 self._authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter * 100) @@ -356,6 +358,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return self._camera_image def _setJobState(self, job_state): + self._last_command = job_state url = QUrl("http://" + self._address + self._api_prefix + "print_job/state") put_request = QNetworkRequest(url) put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") @@ -513,12 +516,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # If this happens before the print has reached progress == 1, the print has # been aborted. if state == "none" or state == "": - if self._print_finished: - state = "printing" - else: + if self._last_command == "abort": self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Aborting print...")) state = "error" - if state == "wait_cleanup" and not self._print_finished: + else: + state = "printing" + if state == "wait_cleanup" and self._last_command == "abort": # Keep showing the "aborted" error state until after the buildplate has been cleaned self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Print aborted. Please check the printer")) state = "error" From 2bbeeb3389a2f699f342b0aa361d17dd486bfaa2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 11 Aug 2016 10:36:21 +0200 Subject: [PATCH 157/438] There is now a hard and a soft discovery mode CURA-2086 --- DiscoverUM3Action.py | 6 +++++- DiscoverUM3Action.qml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 4ca500dc48..7115822582 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -23,8 +23,12 @@ class DiscoverUM3Action(MachineAction): self._network_plugin.addPrinterSignal.connect(self._onPrinterDiscoveryChanged) self._network_plugin.removePrinterSignal.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() + + @pyqtSlot() + def restartDiscovery(self): + if not self._network_plugin: + self.startDiscovery() else: - # Restart bonjour discovery self._network_plugin.startDiscovery() def _onPrinterDiscoveryChanged(self, *args): diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 4dcb5e074d..dcfd76792b 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -70,7 +70,7 @@ Cura.MachineAction { id: rediscoverButton text: catalog.i18nc("@title", "Refresh") - onClicked: manager.startDiscovery() + onClicked: manager.restartDiscovery() anchors.right: parent.right anchors.rightMargin: parent.width * 0.5 } From d0a2b07587a8bc102ba647d8fcc33cb045981237 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 11 Aug 2016 10:45:01 +0200 Subject: [PATCH 158/438] Re-scan no longer forces a re-connect CURA-2086 --- NetworkPrinterOutputDevicePlugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index dbb40e4d77..bd930493be 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -19,6 +19,10 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._browser = None self._printers = {} + # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces + # authentication requests. + self._old_printers = [] + # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addPrinterSignal.connect(self.addPrinter) self.removePrinterSignal.connect(self.removePrinter) @@ -35,6 +39,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if self._browser: self._browser.cancel() self._browser = None + self._old_printers = [printer_name for printer_name in self._printers] self._printers = {} self._zero_conf.__init__() @@ -66,8 +71,9 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._printers[printer.getKey()] = printer global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"): - self._printers[printer.getKey()].connect() - printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced? + self._printers[printer.getKey()].connect() + printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) def removePrinter(self, name): printer = self._printers.pop(name, None) From 842d4b9ad86f64d6fb88aaa13eb191392d203f62 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 11 Aug 2016 15:34:37 +0200 Subject: [PATCH 159/438] Added retry action to auth failed message CURA-2086 --- NetworkPrinterOutputDevice.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 0c34a9ffcc..b100dfd7db 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -125,6 +125,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_key = None self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please aprove the request on the printer"), lifetime = 0, dismissable = False, progress = 0) + self._authentication_failed_message = None + self._camera_image = QImage() self._material_post_objects = {} @@ -203,8 +205,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif auth_state == AuthState.AuthenticationDenied: self.setAcceptsCommands(False) self._authentication_requested_message.hide() - authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed. This can be either due to a timeout or the printer refused the request.")) - authentication_failed_message.show() + self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed. This can be either due to a timeout or the printer refused the request.")) + self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, "Re-send the authentication request") + self._authentication_failed_message.actionTriggered.connect(self.messageActionTriggered) + self._authentication_failed_message.show() # Stop waiting for a response self._authentication_timer.stop() @@ -212,6 +216,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_state = auth_state + def messageActionTriggered(self, message_id, action_id): + self._authentication_failed_message.hide() + self._authentication_state = AuthState.NotAuthenticated + self._authentication_counter = 0 + self._authentication_requested_message.setProgress(0) + self._authentication_id = None + self._authentication_key = None + ## Request data from the connected device. def _update(self): # Check that we aren't in a timeout state From d7c6f85bcdec33e5b779026576e69edb22518c35 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 11 Aug 2016 15:53:45 +0200 Subject: [PATCH 160/438] Add i18n to tooltip and tweak pair request failed message CURA-2086 --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index b100dfd7db..e5bf330654 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -205,8 +205,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif auth_state == AuthState.AuthenticationDenied: self.setAcceptsCommands(False) self._authentication_requested_message.hide() - self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed. This can be either due to a timeout or the printer refused the request.")) - self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, "Re-send the authentication request") + self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed due to a timeout or the printer refused the request.")) + self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the authentication request")) self._authentication_failed_message.actionTriggered.connect(self.messageActionTriggered) self._authentication_failed_message.show() From c362c2e0b9bdcc4ac4935cadfcc7a4bb620e9a95 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 15 Aug 2016 11:04:26 +0200 Subject: [PATCH 161/438] Correct spelling --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e5bf330654..9405c9a629 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -124,7 +124,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_id = None self._authentication_key = None - self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please aprove the request on the printer"), lifetime = 0, dismissable = False, progress = 0) + self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please approve the request on the printer"), lifetime = 0, dismissable = False, progress = 0) self._authentication_failed_message = None self._camera_image = QImage() From dd649580f5a71a3a067e1c5a08734bcb712f9fcd Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 16 Aug 2016 16:46:35 +0200 Subject: [PATCH 162/438] Move the "Changes on Printer" dialog to JediWifiPrinting ...to have full control over the terminology CURA-2116 --- NetworkPrinterOutputDevice.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e5bf330654..66db24bc85 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -12,6 +12,7 @@ from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot from PyQt5.QtGui import QImage +from PyQt5.QtWidgets import QMessageBox import json import os @@ -628,3 +629,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): else: self._progress_message.setProgress(0) self._progress_message.hide() + + ## Let the user decide if the hotends and/or material should be synced with the printer + def materialHotendChangedMessage(self, callback): + Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Changes on the Printer"), + i18n_catalog.i18nc("@label", + "Do you want to change the PrintCores and materials to match your printer?"), + i18n_catalog.i18nc("@label", + "The materials and / or hotends on your printer were changed. For best results always slice for the PrintCores and materials that are inserted in your printer."), + buttons=QMessageBox.Yes + QMessageBox.No, + icon=QMessageBox.Question, + callback=callback + ) \ No newline at end of file From 631d079e264d1d33362142fb9c66817cfd1fb66c Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 16 Aug 2016 17:57:07 +0200 Subject: [PATCH 163/438] Show only one "Successfully paired" message at once. --- NetworkPrinterOutputDevice.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 1e2487ba9d..66611a3a4a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -126,7 +126,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_key = None self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please approve the request on the printer"), lifetime = 0, dismissable = False, progress = 0) - self._authentication_failed_message = None + self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed due to a timeout or the printer refused the request.")) + self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the authentication request")) + self._authentication_failed_message.actionTriggered.connect(self.messageActionTriggered) + self._authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) self._camera_image = QImage() @@ -194,8 +197,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setAcceptsCommands(True) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}.").format(self.name)) self._authentication_requested_message.hide() - authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) - authentication_succeeded_message.show() + self._authentication_succeeded_message.show() # Stop waiting for a response self._authentication_timer.stop() @@ -206,9 +208,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif auth_state == AuthState.AuthenticationDenied: self.setAcceptsCommands(False) self._authentication_requested_message.hide() - self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed due to a timeout or the printer refused the request.")) - self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the authentication request")) - self._authentication_failed_message.actionTriggered.connect(self.messageActionTriggered) self._authentication_failed_message.show() # Stop waiting for a response @@ -290,6 +289,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_counter = 0 self._authentication_timer.stop() + self._authentication_requested_message.hide() + self._authentication_failed_message.hide() + self._authentication_succeeded_message.hide() + if self._error_message: self._error_message.hide() From 58115ce798296bb43155df993a847f688230d47e Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 17 Aug 2016 11:07:39 +0200 Subject: [PATCH 164/438] Fixed one more occurrence of "hotend" to "PrintCore" CURA-2116 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 66611a3a4a..96fb3f050d 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -639,7 +639,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): i18n_catalog.i18nc("@label", "Do you want to change the PrintCores and materials to match your printer?"), i18n_catalog.i18nc("@label", - "The materials and / or hotends on your printer were changed. For best results always slice for the PrintCores and materials that are inserted in your printer."), + "The PrintCores and/or materials on your printer were changed. For best results always slice for the PrintCores and materials that are inserted in your printer."), buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=callback From 8cc9e8cf723169511b3a15f307e2db97eaa2c146 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 17 Aug 2016 12:38:51 +0200 Subject: [PATCH 165/438] Post requests are now aborted when connection is timed out CURA-1851 --- NetworkPrinterOutputDevice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 96fb3f050d..93fedf0c85 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -482,6 +482,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the printer") self._connection_state_before_timeout = self._connection_state + # Check if we were uploading something. Abort if this is the case. + # Some operating systems handle this themselves, others give weird issues. + if self._post_reply: + self._post_reply.abort() self.setConnectionState(ConnectionState.error) return From d405afb9f26493b592d66b645d1c13a95cde4a43 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 17 Aug 2016 12:40:35 +0200 Subject: [PATCH 166/438] Added bit more logging --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 93fedf0c85..ac8fe7fa78 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -499,6 +499,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not status_code: + Logger.log("d", "A reply without a status code was received. Dropping the message") # Received no or empty reply return reply_url = reply.url().toString() From 26eaedaa2cebca06deabdbe3a638c865a8680b0b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 17 Aug 2016 13:00:07 +0200 Subject: [PATCH 167/438] Improved verbosity of logging --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index ac8fe7fa78..ce078c59e0 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -490,7 +490,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. - Logger.log("d", "We got a response from the server after %0.1f of silence", time() - self._last_response_time) + Logger.log("d", "We got a response from the server after %0.1f of silence. Going back to previous state %s", time() - self._last_response_time, self._connection_state_before_timeout) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None @@ -499,7 +499,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not status_code: - Logger.log("d", "A reply without a status code was received. Dropping the message") + Logger.log("d", "A reply from %s did not have status code.", reply.url().toString()) # Received no or empty reply return reply_url = reply.url().toString() From f60871e0684cb15b6343cdb30df0d1286c2a1401 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 17 Aug 2016 13:07:20 +0200 Subject: [PATCH 168/438] Both timeout cases now abort the upload CURA-1851 --- NetworkPrinterOutputDevice.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index ce078c59e0..b98d67bbae 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -234,6 +234,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your network-connections.")) self._connection_message.show() + # Check if we were uploading something. Abort if this is the case. + # Some operating systems handle this themselves, others give weird issues. + if self._post_reply: + self._post_reply.abort() + self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + self._progress_message.hide() self.setConnectionState(ConnectionState.error) if self._authentication_state == AuthState.NotAuthenticated: @@ -486,6 +492,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Some operating systems handle this themselves, others give weird issues. if self._post_reply: self._post_reply.abort() + self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + self._progress_message.hide() + self.setConnectionState(ConnectionState.error) return From 1dbd789d108721b66aee47ac5ad2b30b6bd76b50 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 17 Aug 2016 13:09:04 +0200 Subject: [PATCH 169/438] Only log messages without status when we are not in error mode. This prevents spamming the logs with messages --- NetworkPrinterOutputDevice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index b98d67bbae..7b3c4760b7 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -508,7 +508,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not status_code: - Logger.log("d", "A reply from %s did not have status code.", reply.url().toString()) + if self._connection_state != ConnectionState.error: + Logger.log("d", "A reply from %s did not have status code.", reply.url().toString()) # Received no or empty reply return reply_url = reply.url().toString() From 99892ffd58f33a7da218157c1928d27073953f18 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 18 Aug 2016 13:23:29 +0200 Subject: [PATCH 170/438] Added exception handling for disconnect issues on mac CURA-1851 --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 7b3c4760b7..c8ec7bd299 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -238,9 +238,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Some operating systems handle this themselves, others give weird issues. if self._post_reply: self._post_reply.abort() - self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + try: + self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + except TypeError: + pass # The disconnection can fail on mac in some cases. Ignore that. self._progress_message.hide() self.setConnectionState(ConnectionState.error) + return if self._authentication_state == AuthState.NotAuthenticated: self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. From edabe9d593ebc09cf263bc6e591f8500f33c711a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 19 Aug 2016 13:13:22 +0200 Subject: [PATCH 171/438] Improved logging a bit more CURA-1851 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index c8ec7bd299..f2a16bc5b9 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -503,7 +503,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. - Logger.log("d", "We got a response from the server after %0.1f of silence. Going back to previous state %s", time() - self._last_response_time, self._connection_state_before_timeout) + Logger.log("d", "We got a response (%s) from the server after %0.1f of silence. Going back to previous state %s", reply.url().toString(), time() - self._last_response_time, self._connection_state_before_timeout) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None From 5c2ac558eae92f08f58881ff695c078eb0c11c7a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 19 Aug 2016 13:14:10 +0200 Subject: [PATCH 172/438] Increased timeout time to 10 seconds CURA-1851 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f2a16bc5b9..99ab821d5e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -137,7 +137,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_state_before_timeout = None self._last_response_time = time() - self._response_timeout_time = 5 + self._response_timeout_time = 10 self._not_authenticated_message = None self._last_command = "" From 1d6732273ca934fd035e3c2302681c7260b3b902 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 19 Aug 2016 13:32:17 +0200 Subject: [PATCH 173/438] Added local network connectivity detection CURA-1851 --- NetworkPrinterOutputDevice.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 99ab821d5e..c40f7a8df7 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -180,6 +180,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return self._address def _update_camera(self): + if not self._manager.networkAccessible(): + return ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") image_request = QNetworkRequest(url) @@ -226,13 +228,24 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Request data from the connected device. def _update(self): + # Check if we have an connection in the first place. + if not self._manager.networkAccessible(): + if not self._connection_state_before_timeout: + Logger.log("d", "The network connection seems to be disabled. Going into timeout mode") + self._connection_state_before_timeout = self._connection_state + self.setConnectionState(ConnectionState.error) + self._connection_message = Message(i18n_catalog.i18nc("@info:status", + "The connection with the network was lost.")) + self._connection_message.show() + return + # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accessible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state - self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your network-connections.")) + self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your printer to see if it is connected.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. From 42bbf2b4c840cf63fde3de520b359b33ac04c65a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 19 Aug 2016 15:26:37 +0200 Subject: [PATCH 174/438] ALso abort if the network is not accesible CURA-1851 --- NetworkPrinterOutputDevice.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index c40f7a8df7..6063ab6fee 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -237,6 +237,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the network was lost.")) self._connection_message.show() + # Check if we were uploading something. Abort if this is the case. + # Some operating systems handle this themselves, others give weird issues. + if self._post_reply: + self._post_reply.abort() + try: + self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + except TypeError: + pass # The disconnection can fail on mac in some cases. Ignore that. + self._progress_message.hide() return # Check that we aren't in a timeout state From 0a0870792237f9425a66017fc1219f66b995f197 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 19 Aug 2016 15:30:34 +0200 Subject: [PATCH 175/438] Added logging about network accessible state CURA-1851 --- NetworkPrinterOutputDevice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 6063ab6fee..f2f771122f 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -88,6 +88,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes self._post_request = None self._post_reply = None @@ -142,6 +143,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._last_command = "" + def _onNetworkAccesibleChanged(self, accessible): + Logger.log("d", "Network accessible state changed to: %s", accessible) + def _onAuthenticationTimer(self): self._authentication_counter += 1 self._authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter * 100) From 5825793f28d16464dce88d421ea418891c20a7da Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 22 Aug 2016 09:49:57 +0200 Subject: [PATCH 176/438] QNetworkManager is now re-created every time we try to connect CURA-1851 --- NetworkPrinterOutputDevice.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f2f771122f..9cdab52881 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -83,12 +83,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setIconName("print") - # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly - # hook itself into the event loop, which results in events never being fired / done. - self._manager = QNetworkAccessManager() - self._manager.finished.connect(self._onFinished) - self._manager.authenticationRequired.connect(self._onAuthenticationRequired) - self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes + self._manager = None self._post_request = None self._post_reply = None @@ -380,6 +375,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Start requesting data from printer def connect(self): self.close() # Ensure that previous connection (if any) is killed. + + if self._manager: + self._manager.finished.disconnect(self._onFinished) + self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) + self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) + + self._manager = QNetworkAccessManager() + self._manager.finished.connect(self._onFinished) + self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes + self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() From 0c931424f96327a0affc5084e71e34036bc43b27 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 22 Aug 2016 14:08:01 +0200 Subject: [PATCH 177/438] Re-create the network manager every 30 seconds of not being connected CURA-1851 --- NetworkPrinterOutputDevice.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 9cdab52881..5347eb0e01 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -134,6 +134,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() self._response_timeout_time = 10 + self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. + self._recreate_network_manager_count = 1 self._not_authenticated_message = None self._last_command = "" @@ -227,6 +229,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Request data from the connected device. def _update(self): + # Connection is in timeout, check if we need to re-start the connection. + # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. + # Re-creating the QNetworkManager seems to fix this issue. + if self._last_response_time and self._connection_state_before_timeout: + if time() - self._last_response_time > self._recreate_network_manager_time * self._recreate_network_manager_count: + self._recreate_network_manager_count += 1 + Logger.log("d", "Timeout lasted over 30 seconds, re-checking connection.") + self._createNetworkManager() + return + # Check if we have an connection in the first place. if not self._manager.networkAccessible(): if not self._connection_state_before_timeout: @@ -246,6 +258,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass # The disconnection can fail on mac in some cases. Ignore that. self._progress_message.hide() return + else: + self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: @@ -282,6 +296,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): print_job_request = QNetworkRequest(url) self._manager.get(print_job_request) + def _createNetworkManager(self): + if self._manager: + self._manager.finished.disconnect(self._onFinished) + self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) + self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) + + self._manager = QNetworkAccessManager() + self._manager.finished.connect(self._onFinished) + self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes + ## Convenience function that gets information from the received json data and converts it to the right internal # values / variables def _spliceJSONData(self): @@ -376,15 +401,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def connect(self): self.close() # Ensure that previous connection (if any) is killed. - if self._manager: - self._manager.finished.disconnect(self._onFinished) - self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) - self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) - - self._manager = QNetworkAccessManager() - self._manager.finished.connect(self._onFinished) - self._manager.authenticationRequired.connect(self._onAuthenticationRequired) - self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes + self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. From 3a48fc23d1bd9da4bd2d5534154de9c0a918fcea Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 23 Aug 2016 13:56:50 +0200 Subject: [PATCH 178/438] Added logging for the time it took to upload a print --- NetworkPrinterOutputDevice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 5347eb0e01..0d99e26583 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -137,6 +137,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 self._not_authenticated_message = None + self._send_gcode_start = time() # Time when the sending of the g-code started. self._last_command = "" @@ -456,6 +457,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # being in progress. def startPrint(self): try: + self._send_gcode_start = time() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() @@ -546,6 +548,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._post_reply: self._post_reply.abort() self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + Logger.log("d", "Uploading of print failed after %s", time() - self._send_gcode_start) self._progress_message.hide() self.setConnectionState(ConnectionState.error) @@ -681,6 +684,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): del self._material_post_objects[id(reply)] elif "print_job" in reply_url: reply.uploadProgress.disconnect(self._onUploadProgress) + Logger.log("d", "Uploading of print succeeded after %s", time() - self._send_gcode_start) self._progress_message.hide() elif reply.operation() == QNetworkAccessManager.PutOperation: From fa8eba78735ddb7fba3173f458356291dbfb0e9e Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 1 Sep 2016 15:14:53 +0200 Subject: [PATCH 179/438] Update wording --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 0d99e26583..a1708f395e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -709,9 +709,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def materialHotendChangedMessage(self, callback): Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Changes on the Printer"), i18n_catalog.i18nc("@label", - "Do you want to change the PrintCores and materials to match your printer?"), + "Do you want to change the PrintCores and materials in Cura to match your printer?"), i18n_catalog.i18nc("@label", - "The PrintCores and/or materials on your printer were changed. For best results always slice for the PrintCores and materials that are inserted in your printer."), + "The PrintCores and/or materials on your printer were changed. For the best result, always slice for the PrintCores and materials that are inserted in your printer."), buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=callback From cd9e049895e0657e9942ac8fedb955da27570637 Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Fri, 2 Sep 2016 11:12:28 +0200 Subject: [PATCH 180/438] CMake: Skip looking for C/C++ compiler --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad48443607..618e8ecbbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -project(JediWifiPrintingPlugin) +project(JediWifiPrintingPlugin NONE) cmake_minimum_required(VERSION 2.8.12) install(FILES From 6770db9c51bc98020698701e218f0479aafb532e Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 5 Sep 2016 13:45:57 +0200 Subject: [PATCH 181/438] Update wording when unable to start a new printjob UM3IC-193 --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index a1708f395e..a5e86483d5 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -363,12 +363,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def requestWrite(self, node, file_name = None, filter_by_machine = False): if self._progress != 0: - self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is still printing. Unable to start a new job.")) + self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to start a new print job because the printer is busy. Please check the printer.")) self._error_message.show() return if self._json_printer_state["status"] != "idle": self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is not idle. Current printer status is %s.") % self._json_printer_state["status"]) + i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._json_printer_state["status"]) self._error_message.show() return elif self._authentication_state != AuthState.Authenticated: From ef667c7ec7bc094fa850612ed25ec68fe000df97 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 6 Sep 2016 14:44:53 +0200 Subject: [PATCH 182/438] If there is no material or correct cartridges inserted into printer, the job is refused CURA-2285 --- NetworkPrinterOutputDevice.py | 38 ++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index a5e86483d5..9af4c69ec2 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -381,12 +381,40 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._print_finished = True self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") - # TODO: Implement all checks. - # Check if cartridges are loaded at all (Error) - #self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"] != "" + print_information = Application.getInstance().getPrintInformation() - # Check if there is material loaded at all (Error) - #self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"] != "" + # TODO: Implement all checks. + # Check if PrintCores / materials are loaded at all (Error) + if print_information.materialLengths[0] != 0: # We need to print with extruder slot 1 + if self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"] == "": + Logger.log("e", "No cartridge loaded in slot 1, unable to start print") + self._error_message = Message( + i18n_catalog.i18nc("@info:status", "Unable to start a new print job, no PowerCore loaded in slot 1")) + self._error_message.show() + return + if self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"] == "": + Logger.log("e", "No material loaded in slot 1, unable to start print") + self._error_message = Message( + i18n_catalog.i18nc("@info:status", + "Unable to start a new print job, no material loaded in slot 1")) + self._error_message.show() + return + + if print_information.materialLengths[1] != 0: # We need to print with extruder slot 2 + if self._json_printer_state["heads"][0]["extruders"][1]["hotend"]["id"] == "": + Logger.log("e", "No cartridge loaded in slot 2, unable to start print") + self._error_message = Message( + i18n_catalog.i18nc("@info:status", + "Unable to start a new print job, no PowerCore loaded in slot 2")) + self._error_message.show() + return + if self._json_printer_state["heads"][0]["extruders"][1]["active_material"]["GUID"] == "": + Logger.log("e", "No material loaded in slot 2, unable to start print") + self._error_message = Message( + i18n_catalog.i18nc("@info:status", + "Unable to start a new print job, no material loaded in slot 2")) + self._error_message.show() + return # Check if there is enough material (Warning) #self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["length_remaining"] From d76927ef5d441c78feba5899ecc7884f6f9239ed Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 09:18:52 +0200 Subject: [PATCH 183/438] Added warning messages for mismatch / not enough material CURA-2285 --- NetworkPrinterOutputDevice.py | 77 ++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 9af4c69ec2..76f1ca097e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -8,6 +8,7 @@ from UM.Message import Message import UM.Settings from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState +import cura.Settings.ExtruderManager from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot @@ -383,8 +384,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): print_information = Application.getInstance().getPrintInformation() - # TODO: Implement all checks. - # Check if PrintCores / materials are loaded at all (Error) + # Check if PrintCores / materials are loaded at all. Any failure in these results in an Error. if print_information.materialLengths[0] != 0: # We need to print with extruder slot 1 if self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"] == "": Logger.log("e", "No cartridge loaded in slot 1, unable to start print") @@ -416,10 +416,71 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message.show() return - # Check if there is enough material (Warning) - #self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["length_remaining"] + warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about. - #TODO: Check if the cartridge is the right ID (give warning otherwise) + # Check if there is enough material. Any failure in these results in a warning. + material_length_1 = self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["length_remaining"] + if material_length_1 != -1 and print_information.materialLengths[0] > material_length_1: + warnings.append("not_enough_material_1") + + material_length_2 = self._json_printer_state["heads"][0]["extruders"][1]["active_material"]["length_remaining"] + if material_length_2 != -1 and print_information.materialLengths[1] > material_length_2: + warnings.append("not_enough_material_2") + + # Check if the right cartridges are loaded. Any failure in these results in a warning. + extruder_manager = cura.Settings.ExtruderManager.getInstance() + if print_information.materialLengths[0] != 0: + variant = extruder_manager.getExtruderStack(0).findContainer({"type": "variant"}) + if variant: + if variant.getId() != self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"]: + warnings.append("hotend_1") + + material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) + if material: + if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"]: + warnings.append("wrong_material_1") + + if print_information.materialLengths[1] != 0: + variant = extruder_manager.getExtruderStack(1).findContainer({"type": "variant"}) + if variant: + if variant.getId() != self._json_printer_state["heads"][0]["extruders"][1]["hotend"]["id"]: + warnings.append("hotend_2") + + material = extruder_manager.getExtruderStack(1).findContainer({"type": "material"}) + if material: + if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][1]["active_material"]["GUID"]: + warnings.append("wrong_material_2") + + if warnings: + text = i18n_catalog.i18nc("@label", "A number of configurations are mismatched. Are you sure you wish to print with the selected configuration?") + detailed_text = "
    " + if "not_enough_material_1" in warnings: + detailed_text += "
  • " + i18n_catalog.i18nc("@label", "Not enough material for spool 1.") + "
  • " + if "not_enough_material_2" in warnings: + detailed_text += "
  • " + i18n_catalog.i18nc("@label", "Not enough material for spool 2.") + "
  • " + if "hotend_1" in warnings: + detailed_text += "
  • " + i18n_catalog.i18nc("@label", + "Different PrintCore selected for extruder 1") + "
  • " + if "hotend_2" in warnings: + detailed_text += "
  • " + i18n_catalog.i18nc("@label", + "Different PrintCore selected for extruder 2") + "
  • " + if "wrong_material_1" in warnings: + detailed_text += "
  • " + i18n_catalog.i18nc("@label", + "Different material selected for extruder 1") + "
  • " + if "wrong_material_2" in warnings: + detailed_text += "
  • " + i18n_catalog.i18nc("@label", + "Different material selected for extruder 1") + "
  • " + + detailed_text += "
" + Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), + text, + detailed_text, + buttons=QMessageBox.Yes + QMessageBox.No, + icon=QMessageBox.Question, + callback=self._configurationCallback + ) + + return self.startPrint() @@ -743,4 +804,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=callback - ) \ No newline at end of file + ) + + def _configurationCallback(self, button): + if button == QMessageBox.Yes: + self.startPrint() From b5d0b32bce66f6ab84e96cfb2e31fb034445801a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 09:48:18 +0200 Subject: [PATCH 184/438] Variant is now matched by name instead of ID CURA-2285 --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 76f1ca097e..f0b6d447a5 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -432,7 +432,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if print_information.materialLengths[0] != 0: variant = extruder_manager.getExtruderStack(0).findContainer({"type": "variant"}) if variant: - if variant.getId() != self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"]: + if variant.getName() != self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"]: warnings.append("hotend_1") material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) @@ -443,7 +443,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if print_information.materialLengths[1] != 0: variant = extruder_manager.getExtruderStack(1).findContainer({"type": "variant"}) if variant: - if variant.getId() != self._json_printer_state["heads"][0]["extruders"][1]["hotend"]["id"]: + if variant.getName() != self._json_printer_state["heads"][0]["extruders"][1]["hotend"]["id"]: warnings.append("hotend_2") material = extruder_manager.getExtruderStack(1).findContainer({"type": "material"}) From f2e93bfd978df91fbcccce293130b5bb378e5e11 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 11:46:55 +0200 Subject: [PATCH 185/438] Added time spent in timeout to re-check connection log CURA-2295 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f0b6d447a5..8ec071cb82 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -237,7 +237,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_response_time and self._connection_state_before_timeout: if time() - self._last_response_time > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 - Logger.log("d", "Timeout lasted over 30 seconds, re-checking connection.") + Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", (time() - self._last_response_time)) self._createNetworkManager() return From 93cc25b4081e4a92feff6dc6bbeab2519e2f3560 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 11:56:19 +0200 Subject: [PATCH 186/438] Added exception handling for when wrapped reply object is already deleted CURA-2295 --- NetworkPrinterOutputDevice.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 8ec071cb82..6ab697ed5e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -252,13 +252,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - self._post_reply.abort() - try: - self._post_reply.uploadProgress.disconnect(self._onUploadProgress) - except TypeError: - pass # The disconnection can fail on mac in some cases. Ignore that. - self._progress_message.hide() + try: + if self._post_reply: + self._post_reply.abort() + try: + self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + except TypeError: + pass # The disconnection can fail on mac in some cases. Ignore that. + self._progress_message.hide() + except RuntimeError: + self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: self._recreate_network_manager_count = 1 @@ -273,13 +276,16 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - self._post_reply.abort() - try: - self._post_reply.uploadProgress.disconnect(self._onUploadProgress) - except TypeError: - pass # The disconnection can fail on mac in some cases. Ignore that. - self._progress_message.hide() + try: + if self._post_reply: + self._post_reply.abort() + try: + self._post_reply.uploadProgress.disconnect(self._onUploadProgress) + except TypeError: + pass # The disconnection can fail on mac in some cases. Ignore that. + self._progress_message.hide() + except RuntimeError: + self._post_reply = None # It can happen that the wrapped c++ object is already deleted. self.setConnectionState(ConnectionState.error) return From d0823fda118dc8e7ff18bb4349ddf905db1fba0f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 12:58:48 +0200 Subject: [PATCH 187/438] If a timeout lasted to long without update, we only re-create network interface once CURA-2295 --- NetworkPrinterOutputDevice.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 6ab697ed5e..6f41f42e15 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -231,13 +231,21 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Request data from the connected device. def _update(self): + if self._last_response_time: + time_since_last_response = time() - self._last_response_time + else: + time_since_last_response = 0 + # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: - if time() - self._last_response_time > self._recreate_network_manager_time * self._recreate_network_manager_count: - self._recreate_network_manager_count += 1 - Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", (time() - self._last_response_time)) + if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: + # It can happen that we had a very long timeout (multiple times the recreate time). + # In that case we should jump through the point that the next update won't be right away. + while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: + self._recreate_network_manager_count += 1 + Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() return @@ -268,9 +276,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: - if time() - self._last_response_time > self._response_timeout_time: + if time_since_last_response > self._response_timeout_time: # Go into timeout state. - Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accessible.", time() - self._last_response_time) + Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accessible.", time_since_last_response) self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your printer to see if it is connected.")) self._connection_message.show() From 357f1186da295535e54902bd68e286c1b271d346 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 13:05:11 +0200 Subject: [PATCH 188/438] Added logging to start of uploading CURA-2295 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 6f41f42e15..07c22268c0 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -563,7 +563,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._send_gcode_start = time() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() - + Logger.log("d", "Started sending g-code to remote printer.") ## Mash the data into single string single_string_file_data = "" for line in self._gcode: From 97892273fc65e59fb4246d68fc3010f594192648 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 14:04:23 +0200 Subject: [PATCH 189/438] Updated logging --- NetworkPrinterOutputDevice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 07c22268c0..eade037315 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -155,6 +155,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _onAuthenticationRequired(self, reply, authenticator): if self._authentication_id is not None and self._authentication_key is not None: + Logger.log("d", "Authentication was required. Setting up authenticator.") authenticator.setUser(self._authentication_id) authenticator.setPassword(self._authentication_key) @@ -194,11 +195,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # \param auth_state \type{AuthState} Enum value representing the new auth state def setAuthenticationState(self, auth_state): if auth_state == AuthState.AuthenticationRequested: + Logger.log("d", "Authentication state changed to authentication requested.") self.setAcceptsCommands(False) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0} without access to control the printer.").format(self.name)) self._authentication_requested_message.show() self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: + Logger.log("d", "Authentication state changed to authenticated") self.setAcceptsCommands(True) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}.").format(self.name)) self._authentication_requested_message.hide() @@ -211,6 +214,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Once we are authenticated we need to send all material profiles. self.sendMaterialProfiles() elif auth_state == AuthState.AuthenticationDenied: + Logger.log("d", "Authentication state changed to authentication denied") self.setAcceptsCommands(False) self._authentication_requested_message.hide() self._authentication_failed_message.show() @@ -606,6 +610,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): ## Check if the authentication request was allowed by the printer. def _checkAuthentication(self): + Logger.log("d", "Checking if authentication is correct.") self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id)))) ## Request a authentication key from the printer so we can be authenticated @@ -801,6 +806,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 + # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get + # timeout responses if this happens. + self._last_response_time = time() if new_progress > self._progress_message.getProgress(): self._progress_message.show() # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) From 1f3c8e093948aeb763ed82b636c650f8433894f2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 14:25:24 +0200 Subject: [PATCH 190/438] If saved authentication is wrong, it's now correctly reset CURA-2295 --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index eade037315..c77ac95a3f 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -232,6 +232,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message.setProgress(0) self._authentication_id = None self._authentication_key = None + self._createNetworkManager() # Re-create network manager to force re-authentication. ## Request data from the connected device. def _update(self): @@ -394,6 +395,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", "Not authenticated to print with this machine. Unable to start a new job.")) self._not_authenticated_message.show() + Logger.log("d", "Attempting to perform an action without authentication. Auth state is %s", self._authentication_state) return Application.getInstance().showPrintMonitor.emit(True) @@ -747,7 +749,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("i", "Not authenticated. Attempting to request authentication") self._requestAuthentication() elif status_code == 403: - pass + # If we already had an auth (eg; didn't request one), we only need a single 403 to see it as denied. + if self._authentication_state != AuthState.AuthenticationRequested: + self.setAuthenticationState(AuthState.AuthenticationDenied) elif status_code == 200: self.setAuthenticationState(AuthState.Authenticated) global_container_stack = Application.getInstance().getGlobalContainerStack() From 65a2cedf9c8202524b733ad3f2f0493e51bd626e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 7 Sep 2016 15:05:07 +0200 Subject: [PATCH 191/438] Cleaned up code duplication in warning system CURA-2285 --- NetworkPrinterOutputDevice.py | 105 ++++++++++------------------------ 1 file changed, 31 insertions(+), 74 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index c77ac95a3f..de78a03467 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -405,91 +405,48 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): print_information = Application.getInstance().getPrintInformation() # Check if PrintCores / materials are loaded at all. Any failure in these results in an Error. - if print_information.materialLengths[0] != 0: # We need to print with extruder slot 1 - if self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"] == "": - Logger.log("e", "No cartridge loaded in slot 1, unable to start print") - self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job, no PowerCore loaded in slot 1")) - self._error_message.show() - return - if self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"] == "": - Logger.log("e", "No material loaded in slot 1, unable to start print") - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Unable to start a new print job, no material loaded in slot 1")) - self._error_message.show() - return - - if print_information.materialLengths[1] != 0: # We need to print with extruder slot 2 - if self._json_printer_state["heads"][0]["extruders"][1]["hotend"]["id"] == "": - Logger.log("e", "No cartridge loaded in slot 2, unable to start print") - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Unable to start a new print job, no PowerCore loaded in slot 2")) - self._error_message.show() - return - if self._json_printer_state["heads"][0]["extruders"][1]["active_material"]["GUID"] == "": - Logger.log("e", "No material loaded in slot 2, unable to start print") - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Unable to start a new print job, no material loaded in slot 2")) - self._error_message.show() - return + for index in range(0, self._num_extruders): + if print_information.materialLengths[index] != 0: + if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "": + Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1) + self._error_message = Message( + i18n_catalog.i18nc("@info:status", "Unable to start a new print job; no PrinterCore loaded in slot {0}".format(index + 1))) + self._error_message.show() + return + if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] == "": + Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1) + self._error_message = Message( + i18n_catalog.i18nc("@info:status", + "Unable to start a new print job; no material loaded in slot {0}".format(index + 1))) + self._error_message.show() + return warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about. - # Check if there is enough material. Any failure in these results in a warning. - material_length_1 = self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["length_remaining"] - if material_length_1 != -1 and print_information.materialLengths[0] > material_length_1: - warnings.append("not_enough_material_1") + for index in range(0, self._num_extruders): + # Check if there is enough material. Any failure in these results in a warning. + material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"] + if material_length != -1 and print_information.materialLengths[index] > material_length: + warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1)) - material_length_2 = self._json_printer_state["heads"][0]["extruders"][1]["active_material"]["length_remaining"] - if material_length_2 != -1 and print_information.materialLengths[1] > material_length_2: - warnings.append("not_enough_material_2") - - # Check if the right cartridges are loaded. Any failure in these results in a warning. - extruder_manager = cura.Settings.ExtruderManager.getInstance() - if print_information.materialLengths[0] != 0: - variant = extruder_manager.getExtruderStack(0).findContainer({"type": "variant"}) - if variant: - if variant.getName() != self._json_printer_state["heads"][0]["extruders"][0]["hotend"]["id"]: - warnings.append("hotend_1") + # Check if the right cartridges are loaded. Any failure in these results in a warning. + extruder_manager = cura.Settings.ExtruderManager.getInstance() + if print_information.materialLengths[index] != 0: + variant = extruder_manager.getExtruderStack(0).findContainer({"type": "variant"}) + if variant: + if variant.getName() != self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]: + warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore selected for extruder {0}".format(index + 1))) material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) if material: - if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][0]["active_material"]["GUID"]: - warnings.append("wrong_material_1") - - if print_information.materialLengths[1] != 0: - variant = extruder_manager.getExtruderStack(1).findContainer({"type": "variant"}) - if variant: - if variant.getName() != self._json_printer_state["heads"][0]["extruders"][1]["hotend"]["id"]: - warnings.append("hotend_2") - - material = extruder_manager.getExtruderStack(1).findContainer({"type": "material"}) - if material: - if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][1]["active_material"]["GUID"]: - warnings.append("wrong_material_2") + if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"]: + warnings.append(i18n_catalog.i18nc("@label", "Different material selected for extruder {0}").format(index + 1)) if warnings: text = i18n_catalog.i18nc("@label", "A number of configurations are mismatched. Are you sure you wish to print with the selected configuration?") detailed_text = "
    " - if "not_enough_material_1" in warnings: - detailed_text += "
  • " + i18n_catalog.i18nc("@label", "Not enough material for spool 1.") + "
  • " - if "not_enough_material_2" in warnings: - detailed_text += "
  • " + i18n_catalog.i18nc("@label", "Not enough material for spool 2.") + "
  • " - if "hotend_1" in warnings: - detailed_text += "
  • " + i18n_catalog.i18nc("@label", - "Different PrintCore selected for extruder 1") + "
  • " - if "hotend_2" in warnings: - detailed_text += "
  • " + i18n_catalog.i18nc("@label", - "Different PrintCore selected for extruder 2") + "
  • " - if "wrong_material_1" in warnings: - detailed_text += "
  • " + i18n_catalog.i18nc("@label", - "Different material selected for extruder 1") + "
  • " - if "wrong_material_2" in warnings: - detailed_text += "
  • " + i18n_catalog.i18nc("@label", - "Different material selected for extruder 1") + "
  • " + for warning in warnings: + detailed_text += "
  • " + warning + "
  • " detailed_text += "
" Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), From 9bd4d3c0a8d005f0aae0fc8086046347f2738acf Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 8 Sep 2016 11:26:01 +0200 Subject: [PATCH 192/438] Update configuration mismatch dialog layout and wording Also return to settings tab when not continuing with the print CURA-2285 --- NetworkPrinterOutputDevice.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index de78a03467..e6964d9715 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -440,27 +440,35 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) if material: if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"]: - warnings.append(i18n_catalog.i18nc("@label", "Different material selected for extruder {0}").format(index + 1)) + warnings.append(i18n_catalog.i18nc("@label", "Different material selected for extruder {0}").format(index + 1)) if warnings: - text = i18n_catalog.i18nc("@label", "A number of configurations are mismatched. Are you sure you wish to print with the selected configuration?") - detailed_text = "
    " + text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") + informative_text = i18n_catalog.i18nc("@label", "There is a mismatch between the configuration of the printer and Cura. " + "For the best result, always slice for the PrintCores and materials that are inserted in your printer.") + detailed_text = "" for warning in warnings: - detailed_text += "
  • " + warning + "
  • " + detailed_text += warning + "\n" - detailed_text += "
" Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, + informative_text, detailed_text, buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, - callback=self._configurationCallback + callback=self._configurationMismatchMessageCallback ) return self.startPrint() + def _configurationMismatchMessageCallback(self, button): + if button == QMessageBox.Yes: + self.startPrint() + else: + Application.getInstance().showPrintMonitor.emit(False) + def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error @@ -788,7 +796,3 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): icon=QMessageBox.Question, callback=callback ) - - def _configurationCallback(self, button): - if button == QMessageBox.Yes: - self.startPrint() From 28bfc364725c8c5feefc42d1d9693b25503c30ee Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 8 Sep 2016 16:37:10 +0200 Subject: [PATCH 193/438] Recreate network manager count is now always increased once --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index de78a03467..b119f5ff8b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -246,6 +246,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: + self._recreate_network_manager_count += 1 # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: From d0823ebff04ac42679433ac87cb5d4ff51f39aca Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 8 Sep 2016 16:57:11 +0200 Subject: [PATCH 194/438] Added logging when the post was aborted due to connection loss CURA-2295 --- NetworkPrinterOutputDevice.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 208ad80d51..7038c4e672 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -268,11 +268,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: - self._post_reply.abort() + Logger.log("d", "Stopping post upload because the connection was lost.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. + + self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. @@ -292,11 +294,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: - self._post_reply.abort() + Logger.log("d", "Stopping post upload because the connection was lost.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. + + self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. From ad496a646775917c2924aee628667b79ea3f0407 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 8 Sep 2016 17:27:18 +0200 Subject: [PATCH 195/438] Counter is now only reset if no previous state was set --- NetworkPrinterOutputDevice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 7038c4e672..5e16c0d5f7 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -280,7 +280,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: - self._recreate_network_manager_count = 1 + if not self._connection_state_before_timeout: + self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: From 3a4ccf13675ae4828ebd111a5493c1f2216c7ee2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 10:25:12 +0200 Subject: [PATCH 196/438] Uploading g-codes is now done g-zipped CURA-2286 --- NetworkPrinterOutputDevice.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 5e16c0d5f7..f65ef30f9b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -17,6 +17,7 @@ from PyQt5.QtWidgets import QMessageBox import json import os +import gzip from time import time @@ -40,7 +41,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._properties = properties # Properties dict as provided by zero conf self._gcode = None - self._print_finished = True # _print_finsihed == False means we're halfway in a print + self._print_finished = True # _print_finsihed == False means we're halfway in a print + + self._use_gzip = True # Should we use g-zip compression before sending the data? # This holds the full JSON file that was received from the last request. # The JSON looks like: @@ -546,7 +549,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): for line in self._gcode: single_string_file_data += line - file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName + if self._use_gzip: + file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName + single_string_file_data = gzip.compress(single_string_file_data.encode("utf-8")) + else: + file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName + single_string_file_data = single_string_file_data.encode("utf-8") ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) @@ -555,7 +563,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) - self._post_part.setBody(single_string_file_data.encode()) + self._post_part.setBody(single_string_file_data) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "print_job") From dc0124f502090a9a87e31db55ab48ed496e3fe73 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 11:04:16 +0200 Subject: [PATCH 197/438] If serialization of XML fails, we don't try to send it to the printer --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f65ef30f9b..4fef337b13 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -607,7 +607,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material"): try: xml_data = container.serialize() - if xml_data == "": + if xml_data == "" or xml_data is None: continue material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) From f75b4739f68837476e7e6725ca783589a23002d7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 8 Sep 2016 16:57:11 +0200 Subject: [PATCH 198/438] Added logging when the post was aborted due to connection loss CURA-2295 --- NetworkPrinterOutputDevice.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 208ad80d51..7038c4e672 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -268,11 +268,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: - self._post_reply.abort() + Logger.log("d", "Stopping post upload because the connection was lost.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. + + self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. @@ -292,11 +294,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: - self._post_reply.abort() + Logger.log("d", "Stopping post upload because the connection was lost.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. + + self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. From f5886e5ecc64eee4ff4b7d2827b2b1595f64a2ea Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 8 Sep 2016 17:27:18 +0200 Subject: [PATCH 199/438] Counter is now only reset if no previous state was set --- NetworkPrinterOutputDevice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 7038c4e672..5e16c0d5f7 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -280,7 +280,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: - self._recreate_network_manager_count = 1 + if not self._connection_state_before_timeout: + self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: From a6a47b0aa27c331b38134498955e83c1e5e88f06 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 10:25:12 +0200 Subject: [PATCH 200/438] Uploading g-codes is now done g-zipped CURA-2286 --- NetworkPrinterOutputDevice.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 5e16c0d5f7..f65ef30f9b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -17,6 +17,7 @@ from PyQt5.QtWidgets import QMessageBox import json import os +import gzip from time import time @@ -40,7 +41,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._properties = properties # Properties dict as provided by zero conf self._gcode = None - self._print_finished = True # _print_finsihed == False means we're halfway in a print + self._print_finished = True # _print_finsihed == False means we're halfway in a print + + self._use_gzip = True # Should we use g-zip compression before sending the data? # This holds the full JSON file that was received from the last request. # The JSON looks like: @@ -546,7 +549,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): for line in self._gcode: single_string_file_data += line - file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName + if self._use_gzip: + file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName + single_string_file_data = gzip.compress(single_string_file_data.encode("utf-8")) + else: + file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName + single_string_file_data = single_string_file_data.encode("utf-8") ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) @@ -555,7 +563,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) - self._post_part.setBody(single_string_file_data.encode()) + self._post_part.setBody(single_string_file_data) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "print_job") From 97cf79c2e18ce001da1a94a3620b493104e44ab0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 11:04:16 +0200 Subject: [PATCH 201/438] If serialization of XML fails, we don't try to send it to the printer --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f65ef30f9b..4fef337b13 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -607,7 +607,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material"): try: xml_data = container.serialize() - if xml_data == "": + if xml_data == "" or xml_data is None: continue material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) From 9de064fdba7cb5490b48979ad4b6b7f5743f0c07 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 15:20:08 +0200 Subject: [PATCH 202/438] Fixed issue where large files could no longer be sent CURA-2286 --- NetworkPrinterOutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 4fef337b13..7bb1eb59d9 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -552,6 +552,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._use_gzip: file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName single_string_file_data = gzip.compress(single_string_file_data.encode("utf-8")) + # Pretend that this is a response, as zipping might take a bit of time. + self._last_response_time = time() else: file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName single_string_file_data = single_string_file_data.encode("utf-8") From 3747db0f4daf4759e4025907981baec691e687ce Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 15:28:37 +0200 Subject: [PATCH 203/438] Added logging to see how long compression took CURA-2286 --- NetworkPrinterOutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 7bb1eb59d9..c606546e3e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -551,7 +551,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._use_gzip: file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName + compress_time = time() single_string_file_data = gzip.compress(single_string_file_data.encode("utf-8")) + Logger.log("d", "It took %s seconds to compress the file", time() - compress_time) # Pretend that this is a response, as zipping might take a bit of time. self._last_response_time = time() else: From 7f0194cce01d1a50f01b4e95f05ce33d9a59403b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 15:44:02 +0200 Subject: [PATCH 204/438] We now compress the g-code layer by layer so the GIL won't always lock fully CURA-2286 --- NetworkPrinterOutputDevice.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index c606546e3e..d5f74f5787 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -18,8 +18,10 @@ from PyQt5.QtWidgets import QMessageBox import json import os import gzip +import zlib from time import time +from time import sleep i18n_catalog = i18nCatalog("cura") @@ -544,21 +546,22 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() Logger.log("d", "Started sending g-code to remote printer.") + ## Mash the data into single string - single_string_file_data = "" + byte_array_file_data = b"" for line in self._gcode: - single_string_file_data += line + if self._use_gzip: + byte_array_file_data += gzip.compress(line.encode("utf-8")) + sleep(0) # Yield. + # Pretend that this is a response, as zipping might take a bit of time. + self._last_response_time = time() + else: + byte_array_file_data += line.encode("utf-8") if self._use_gzip: file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName - compress_time = time() - single_string_file_data = gzip.compress(single_string_file_data.encode("utf-8")) - Logger.log("d", "It took %s seconds to compress the file", time() - compress_time) - # Pretend that this is a response, as zipping might take a bit of time. - self._last_response_time = time() else: file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName - single_string_file_data = single_string_file_data.encode("utf-8") ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) @@ -567,7 +570,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) - self._post_part.setBody(single_string_file_data) + self._post_part.setBody(byte_array_file_data) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "print_job") From f294d580a98cf8837bb5770d60e8cd8a37aaef2c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 9 Sep 2016 16:46:04 +0200 Subject: [PATCH 205/438] Zipping the data no longer causes GUI to freeze CURA-2286 --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index d5f74f5787..0624c817d1 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -11,7 +11,7 @@ from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState import cura.Settings.ExtruderManager from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply -from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot +from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot, QCoreApplication from PyQt5.QtGui import QImage from PyQt5.QtWidgets import QMessageBox @@ -552,7 +552,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): for line in self._gcode: if self._use_gzip: byte_array_file_data += gzip.compress(line.encode("utf-8")) - sleep(0) # Yield. + QCoreApplication.processEvents() # Ensure that the GUI does not freeze. # Pretend that this is a response, as zipping might take a bit of time. self._last_response_time = time() else: From ae161748ef9294bb065ad6800bedb9851f134c56 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 12 Sep 2016 10:39:58 +0200 Subject: [PATCH 206/438] Material type is now only checked if we actually use that extruder CURA-2313 --- NetworkPrinterOutputDevice.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 0624c817d1..ce0aba70af 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -448,10 +448,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if variant.getName() != self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]: warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore selected for extruder {0}".format(index + 1))) - material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) - if material: - if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"]: - warnings.append(i18n_catalog.i18nc("@label", "Different material selected for extruder {0}").format(index + 1)) + material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) + if material: + if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"]: + warnings.append(i18n_catalog.i18nc("@label", "Different material selected for extruder {0}").format(index + 1)) if warnings: text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") From 820d91486f192c4785a7291d574def4e063a53d2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 12 Sep 2016 10:44:56 +0200 Subject: [PATCH 207/438] Printer setting mismatches are now also logged as warnings CURA-2313 --- NetworkPrinterOutputDevice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index ce0aba70af..850e893d96 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -438,6 +438,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check if there is enough material. Any failure in these results in a warning. material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"] if material_length != -1 and print_information.materialLengths[index] > material_length: + Logger.log("w", "Printer reports that there is not enough material left for extruder %s. We need %s and the printer has %s", index + 1, print_information.materialLengths[index], material_length) warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1)) # Check if the right cartridges are loaded. Any failure in these results in a warning. @@ -446,11 +447,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): variant = extruder_manager.getExtruderStack(0).findContainer({"type": "variant"}) if variant: if variant.getName() != self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]: + Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"], variant.getName()) warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore selected for extruder {0}".format(index + 1))) material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) if material: if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"]: + Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, + self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"], + material.getMetaDataEntry("GUID")) warnings.append(i18n_catalog.i18nc("@label", "Different material selected for extruder {0}").format(index + 1)) if warnings: @@ -469,7 +474,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): icon=QMessageBox.Question, callback=self._configurationMismatchMessageCallback ) - return self.startPrint() From feae612d5511a2a402619b05b501f10241372897 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 12 Sep 2016 10:54:57 +0200 Subject: [PATCH 208/438] Look at the correct stack for variant CURA-2313 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 850e893d96..627f0b7df3 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -444,7 +444,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Check if the right cartridges are loaded. Any failure in these results in a warning. extruder_manager = cura.Settings.ExtruderManager.getInstance() if print_information.materialLengths[index] != 0: - variant = extruder_manager.getExtruderStack(0).findContainer({"type": "variant"}) + variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"}) if variant: if variant.getName() != self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]: Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"], variant.getName()) From f914ef5424308764f0e53c05fbb1fb64909a93bd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 12 Sep 2016 11:59:25 +0200 Subject: [PATCH 209/438] We now save the auth data right away in order to prevent mismatches CURA-2279 --- NetworkPrinterOutputDevice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 627f0b7df3..956d4bc2e9 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -755,6 +755,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id) else: global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id) + Application.getInstance().saveStack(global_container_stack) # Force save so we are sure the data is not lost. Logger.log("i", "Authentication succeeded") else: # Got a response that we didn't expect, so something went wrong. Logger.log("w", "While trying to authenticate, we got an unexpected response: %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) @@ -775,6 +776,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if "/auth/request" in reply_url: # We got a response to requesting authentication. data = json.loads(bytes(reply.readAll()).decode("utf-8")) + + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack: # Remove any old data. + global_container_stack.removeMetaDataEntry("network_authentication_key") + global_container_stack.removeMetaDataEntry("network_authentication_id") + Application.getInstance().saveStack(global_container_stack) # Force saving so we don't keep wrong auth data. + self._authentication_key = data["key"] self._authentication_id = data["id"] Logger.log("i", "Got a new authentication ID. Waiting for authorization: %s", self._authentication_id ) From 1d91827664e0857c0a031ba53db14c819eeb94a8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 12 Sep 2016 13:31:07 +0200 Subject: [PATCH 210/438] NetworkPrinter now uses printerState to store global state CURA-2235 --- NetworkPrinterOutputDevice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 956d4bc2e9..919df506d4 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -364,6 +364,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): head_y = self._json_printer_state["heads"][0]["position"]["y"] head_z = self._json_printer_state["heads"][0]["position"]["z"] self._updateHeadPosition(head_x, head_y, head_z) + self._updatePrinterState(self._json_printer_state["status"]) + def close(self): self._updateJobState("") @@ -397,9 +399,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to start a new print job because the printer is busy. Please check the printer.")) self._error_message.show() return - if self._json_printer_state["status"] != "idle": + if self._printer_state != "idle": self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._json_printer_state["status"]) + i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._printer_state) self._error_message.show() return elif self._authentication_state != AuthState.Authenticated: From e09de5257a8733765ee06d824dc3237a6c40201d Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 12 Sep 2016 13:36:39 +0200 Subject: [PATCH 211/438] Fix text about connection state in print monitor --- NetworkPrinterOutputDevice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 0624c817d1..032010b4e8 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -202,7 +202,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if auth_state == AuthState.AuthenticationRequested: Logger.log("d", "Authentication state changed to authentication requested.") self.setAcceptsCommands(False) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0} without access to control the printer.").format(self.name)) + self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}, waiting for access to control the printer.").format(self.name)) self._authentication_requested_message.show() self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: @@ -221,6 +221,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif auth_state == AuthState.AuthenticationDenied: Logger.log("d", "Authentication state changed to authentication denied") self.setAcceptsCommands(False) + self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}, no access to control the printer.").format(self.name)) self._authentication_requested_message.hide() self._authentication_failed_message.show() From 220abd2cdcb2cab672a53b69b415a7ff3ea95865 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 12 Sep 2016 13:49:00 +0200 Subject: [PATCH 212/438] Fix i18n context --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index b030fe61e6..6b7457ad36 100644 --- a/__init__.py +++ b/__init__.py @@ -10,7 +10,7 @@ def getMetaData(): "plugin": { "name": "UM3 Network Connection", "author": "Ultimaker", - "description": catalog.i18nc("Wifi connection", "UM3 Network Connection"), + "description": catalog.i18nc("@info:whatsthis", "Manages network connections to Ultimaker 3 printers"), "api": 3 } } From af4cfac6acf27376fa0599d801a890d8964c86b2 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 12 Sep 2016 14:19:34 +0200 Subject: [PATCH 213/438] Add a link to a network connection troubleshooting guide CURA-2035 --- DiscoverUM3Action.qml | 118 ++++++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 49 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index dcfd76792b..a33ddd31fe 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -77,70 +77,90 @@ Cura.MachineAction Row { + id: contentRow width: parent.width spacing: UM.Theme.getSize("default_margin").width - ScrollView + + Column { - id: objectListContainer - frameVisible: true width: parent.width * 0.5 - height: base.height - parent.y + spacing: UM.Theme.getSize("default_margin").height - Rectangle + ScrollView { - parent: viewport - anchors.fill: parent - color: palette.light - } - - ListView - { - id: listview - model: manager.foundDevices - onModelChanged: - { - var selectedKey = manager.getStoredKey(); - for(var i = 0; i < model.length; i++) { - if(model[i].getKey() == selectedKey) - { - currentIndex = i; - return - } - } - currentIndex = -1; - } + id: objectListContainer + frameVisible: true width: parent.width - currentIndex: -1 - onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] - Component.onCompleted: manager.startDiscovery() - delegate: Rectangle - { - height: childrenRect.height - color: ListView.isCurrentItem ? palette.highlight : index % 2 ? palette.base : palette.alternateBase - width: parent.width - Label - { - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right: parent.right - text: listview.model[index].name - color: parent.ListView.isCurrentItem ? palette.highlightedText : palette.text - elide: Text.ElideRight - } + height: base.height - contentRow.y - discoveryTip.height - MouseArea + Rectangle + { + parent: viewport + anchors.fill: parent + color: palette.light + } + + ListView + { + id: listview + model: manager.foundDevices + onModelChanged: { - anchors.fill: parent; - onClicked: - { - if(!parent.ListView.isCurrentItem) + var selectedKey = manager.getStoredKey(); + for(var i = 0; i < model.length; i++) { + if(model[i].getKey() == selectedKey) { - parent.ListView.view.currentIndex = index; + currentIndex = i; + return + } + } + currentIndex = -1; + } + width: parent.width + currentIndex: -1 + onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] + Component.onCompleted: manager.startDiscovery() + delegate: Rectangle + { + height: childrenRect.height + color: ListView.isCurrentItem ? palette.highlight : index % 2 ? palette.base : palette.alternateBase + width: parent.width + Label + { + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + text: listview.model[index].name + color: parent.ListView.isCurrentItem ? palette.highlightedText : palette.text + elide: Text.ElideRight + } + + MouseArea + { + anchors.fill: parent; + onClicked: + { + if(!parent.ListView.isCurrentItem) + { + parent.ListView.view.currentIndex = index; + } } } } } } + Label + { + id: discoveryTip + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.WordWrap + //: Tips label + //TODO: get actual link from webteam + text: catalog.i18nc("@label", "If your Ultimaker 3 is not listed, read the Ultimaker 3 network troubleshooting guide").arg("https://ultimaker.com/en/troubleshooting"); + onLinkActivated: Qt.openUrlExternally(link) + } + } Column { From 0e0569a37c5d52b4ddf778cac45ec8f6bfef502b Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 12 Sep 2016 15:40:43 +0200 Subject: [PATCH 214/438] Update user-facing messages for consistency Make sure we have a consistent terminology; requesting access vs pairing/authentication. --- NetworkPrinterOutputDevice.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index afc5b0e2e1..48a9554a50 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -127,11 +127,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_id = None self._authentication_key = None - self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Requested access. Please approve the request on the printer"), lifetime = 0, dismissable = False, progress = 0) - self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Pairing request failed due to a timeout or the printer refused the request.")) - self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the authentication request")) + self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer requested. Please approve the request on the printer"), lifetime = 0, dismissable = False, progress = 0) + self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "")) + self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) self._authentication_failed_message.actionTriggered.connect(self.messageActionTriggered) - self._authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Printer was successfully paired with Cura")) + self._authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer accepted")) self._camera_image = QImage() @@ -202,7 +202,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if auth_state == AuthState.AuthenticationRequested: Logger.log("d", "Authentication state changed to authentication requested.") self.setAcceptsCommands(False) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}, waiting for access to control the printer.").format(self.name)) + self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}. Please approve the access request on the printer.").format(self.name)) self._authentication_requested_message.show() self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: @@ -221,8 +221,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif auth_state == AuthState.AuthenticationDenied: Logger.log("d", "Authentication state changed to authentication denied") self.setAcceptsCommands(False) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}, no access to control the printer.").format(self.name)) + self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}. No access to control the printer.").format(self.name)) self._authentication_requested_message.hide() + if self._authentication_timer.remainingTime() > 0: + self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request was denied on the printer.")) + else: + self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request failed due to a timeout.")) self._authentication_failed_message.show() # Stop waiting for a response @@ -407,7 +411,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return elif self._authentication_state != AuthState.Authenticated: self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", - "Not authenticated to print with this machine. Unable to start a new job.")) + "No access to print with this printer. Unable to start a new job.")) self._not_authenticated_message.show() Logger.log("d", "Attempting to perform an action without authentication. Auth state is %s", self._authentication_state) return @@ -424,14 +428,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "": Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1) self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job; no PrinterCore loaded in slot {0}".format(index + 1))) + i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No PrinterCore loaded in slot {0}".format(index + 1))) self._error_message.show() return if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] == "": Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1) self._error_message = Message( i18n_catalog.i18nc("@info:status", - "Unable to start a new print job; no material loaded in slot {0}".format(index + 1))) + "Unable to start a new print job. No material loaded in slot {0}".format(index + 1))) self._error_message.show() return From b82889f84e6e772b2eaa5a75da9a51cdf8beb0dc Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 12 Sep 2016 15:59:45 +0200 Subject: [PATCH 215/438] Only show access request message when the user requested access --- NetworkPrinterOutputDevice.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 48a9554a50..674b35343b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -122,6 +122,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval self._authentication_timer.setSingleShot(False) self._authentication_timer.timeout.connect(self._onAuthenticationTimer) + self._authentication_request_active = False self._authentication_state = AuthState.NotAuthenticated self._authentication_id = None @@ -129,9 +130,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer requested. Please approve the request on the printer"), lifetime = 0, dismissable = False, progress = 0) self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "")) - self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry "), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) + self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) self._authentication_failed_message.actionTriggered.connect(self.messageActionTriggered) self._authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer accepted")) + self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job.")) + self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"), None, i18n_catalog.i18nc("@info:tooltip", "Send access request to the printer")) + self._not_authenticated_message.actionTriggered.connect(self.messageActionTriggered) self._camera_image = QImage() @@ -142,7 +146,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._response_timeout_time = 10 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 - self._not_authenticated_message = None + self._send_gcode_start = time() # Time when the sending of the g-code started. self._last_command = "" @@ -204,13 +208,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setAcceptsCommands(False) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}. Please approve the access request on the printer.").format(self.name)) self._authentication_requested_message.show() + self._authentication_request_active = True self._authentication_timer.start() # Start timer so auth will fail after a while. elif auth_state == AuthState.Authenticated: Logger.log("d", "Authentication state changed to authenticated") self.setAcceptsCommands(True) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}.").format(self.name)) self._authentication_requested_message.hide() - self._authentication_succeeded_message.show() + if self._authentication_request_active: + self._authentication_succeeded_message.show() # Stop waiting for a response self._authentication_timer.stop() @@ -223,11 +229,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setAcceptsCommands(False) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}. No access to control the printer.").format(self.name)) self._authentication_requested_message.hide() - if self._authentication_timer.remainingTime() > 0: - self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request was denied on the printer.")) - else: - self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request failed due to a timeout.")) - self._authentication_failed_message.show() + if self._authentication_request_active: + if self._authentication_timer.remainingTime() > 0: + self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request was denied on the printer.")) + else: + self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request failed due to a timeout.")) + + self._authentication_failed_message.show() + self._authentication_request_active = False # Stop waiting for a response self._authentication_timer.stop() @@ -237,6 +246,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def messageActionTriggered(self, message_id, action_id): self._authentication_failed_message.hide() + self._not_authenticated_message.hide() self._authentication_state = AuthState.NotAuthenticated self._authentication_counter = 0 self._authentication_requested_message.setProgress(0) @@ -410,8 +420,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._error_message.show() return elif self._authentication_state != AuthState.Authenticated: - self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", - "No access to print with this printer. Unable to start a new job.")) self._not_authenticated_message.show() Logger.log("d", "Attempting to perform an action without authentication. Auth state is %s", self._authentication_state) return From e94540384512b23f8183e2c483860d1160b046c4 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 12 Sep 2016 18:53:12 +0200 Subject: [PATCH 216/438] Make requesting authentication public Contributes to CURA-2277 --- NetworkPrinterOutputDevice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 674b35343b..5d70005d5f 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -131,11 +131,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer requested. Please approve the request on the printer"), lifetime = 0, dismissable = False, progress = 0) self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "")) self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) - self._authentication_failed_message.actionTriggered.connect(self.messageActionTriggered) + self._authentication_failed_message.actionTriggered.connect(self.requestAuthentication) self._authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer accepted")) self._not_authenticated_message = Message(i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job.")) self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"), None, i18n_catalog.i18nc("@info:tooltip", "Send access request to the printer")) - self._not_authenticated_message.actionTriggered.connect(self.messageActionTriggered) + self._not_authenticated_message.actionTriggered.connect(self.requestAuthentication) self._camera_image = QImage() @@ -244,7 +244,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_state = auth_state - def messageActionTriggered(self, message_id, action_id): + @pyqtSlot() + def requestAuthentication(self, message_id, action_id): self._authentication_failed_message.hide() self._not_authenticated_message.hide() self._authentication_state = AuthState.NotAuthenticated From c5d0942ee29cf2adfdb53b3a09b2c78ba870d788 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 12 Sep 2016 18:54:28 +0200 Subject: [PATCH 217/438] Add "Connect" and "Request Access" buttons to sidebar monitor Functionality is not 100% proven, but the required strings are in CURA-2277 --- DiscoverUM3Action.py | 29 +++++++++++++++-- UM3InfoComponents.qml | 76 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 UM3InfoComponents.qml diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 7115822582..747b568e25 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -1,8 +1,13 @@ from cura.MachineAction import MachineAction from UM.Application import Application +from UM.PluginRegistry import PluginRegistry +from UM.Logger import Logger -from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot +from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QUrl, QObject +from PyQt5.QtQml import QQmlComponent, QQmlContext + +import os.path from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -14,6 +19,12 @@ class DiscoverUM3Action(MachineAction): self._network_plugin = None + self._context = None + self._additional_component = None + self._additional_components_view = None + + Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) + printersChanged = pyqtSignal() @pyqtSlot() @@ -68,4 +79,18 @@ class DiscoverUM3Action(MachineAction): if "um_network_key" in meta_data: return global_container_stack.getMetaDataEntry("um_network_key") - return "" \ No newline at end of file + return "" + + def _createAdditionalComponentsView(self): + Logger.log("d", "Creating additional ui components for UM3.") + + path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("JediWifiPrintingPlugin"), "UM3InfoComponents.qml")) + self._additional_component = QQmlComponent(Application.getInstance()._engine, path) + + # We need access to engine (although technically we can't) + self._context = QQmlContext(Application.getInstance()._engine.rootContext()) + self._context.setContextProperty("manager", self) + self._additional_components_view = self._additional_component.create(self._context) + + Application.getInstance().addAdditionalComponent("monitorButtons", self._additional_components_view.findChild(QObject, "networkPrinterConnectButton")) + Application.getInstance().addAdditionalComponent("machinesDetailPane", self._additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml new file mode 100644 index 0000000000..0106c8cffa --- /dev/null +++ b/UM3InfoComponents.qml @@ -0,0 +1,76 @@ +import UM 1.2 as UM +import Cura 1.0 as Cura + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 + +Item +{ + id: base + + property bool isUM3: Cura.MachineManager.activeDefinitionId == "ultimaker3" + property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 + property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands + + Row + { + objectName: "networkPrinterConnectButton" + visible: isUM3 + spacing: UM.Theme.getSize("default_marging").width + + Button + { + height: UM.Theme.getSize("save_button_save_to_button").height + tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") + text: catalog.i18nc("@action:button", "Request Access") + style: UM.Theme.styles.sidebar_action_button + onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() + visible: base.printerConnected && !base.printerAcceptsCommands + } + + Button + { + height: UM.Theme.getSize("save_button_save_to_button").height + tooltip: catalog.i18nc("@info:tooltip", "Connect to a printer") + text: catalog.i18nc("@action:button", "Connect") + style: UM.Theme.styles.sidebar_action_button + onClicked: connectActionDialog.show() + visible: !base.printerConnected + } + } + + UM.Dialog + { + id: connectActionDialog + Loader + { + anchors.fill: parent + source: "DiscoverUM3Action.qml" + } + rightButtons: Button + { + text: catalog.i18nc("@action:button", "Close") + iconName: "dialog-close" + onClicked: connectActionDialog.reject() + } + } + + + Item + { + objectName: "networkPrinterConnectionInfo" + visible: isUM3 + Button + { + height: UM.Theme.getSize("save_button_save_to_button").height + tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") + text: catalog.i18nc("@action:button", "Request Access") + onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() + visible: base.printerConnected && !base.printerAcceptsCommands + } + } + + UM.I18nCatalog{id: catalog; name:"cura"} +} \ No newline at end of file From 6435e8ce6f9e21440181344292f97b6faedcb5f2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 13 Sep 2016 09:49:17 +0200 Subject: [PATCH 218/438] Request is now only triggered on right actions CURA-2279 --- NetworkPrinterOutputDevice.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 5d70005d5f..239076383d 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -246,14 +246,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): @pyqtSlot() def requestAuthentication(self, message_id, action_id): - self._authentication_failed_message.hide() - self._not_authenticated_message.hide() - self._authentication_state = AuthState.NotAuthenticated - self._authentication_counter = 0 - self._authentication_requested_message.setProgress(0) - self._authentication_id = None - self._authentication_key = None - self._createNetworkManager() # Re-create network manager to force re-authentication. + if action_id == "Request" or action_id == "Retry": + self._authentication_failed_message.hide() + self._not_authenticated_message.hide() + self._authentication_state = AuthState.NotAuthenticated + self._authentication_counter = 0 + self._authentication_requested_message.setProgress(0) + self._authentication_id = None + self._authentication_key = None + self._createNetworkManager() # Re-create network manager to force re-authentication. ## Request data from the connected device. def _update(self): From 6069182175e8841ce08e0dde27052e41b72bd857 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 13 Sep 2016 09:50:04 +0200 Subject: [PATCH 219/438] Added UM3InfoComponents.qml to CMakeLists to the file also gets installed in the build --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 618e8ecbbe..c551e7f707 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ install(FILES DiscoverUM3Action.qml NetworkPrinterOutputDevice.py NetworkPrinterOutputDevicePlugin.py + UM3InfoComponents.qml LICENSE README.md DESTINATION lib/cura/plugins/JediWifiPrintingPlugin From 1ce6c3a4f1737d26f0f9fb269168cea6b8d0ea1c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 13 Sep 2016 10:53:41 +0200 Subject: [PATCH 220/438] Added defaults to requestAcces Cura-2277 --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 239076383d..921a704d46 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -245,7 +245,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_state = auth_state @pyqtSlot() - def requestAuthentication(self, message_id, action_id): + def requestAuthentication(self, message_id = None, action_id = "Retry"): if action_id == "Request" or action_id == "Retry": self._authentication_failed_message.hide() self._not_authenticated_message.hide() From a266a4026c4ee193564dd61aa0e79cadc07e2026 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 13 Sep 2016 11:02:45 +0200 Subject: [PATCH 221/438] Added explicit enabled property so connect button actually reacts to clicks CURA-2277 --- UM3InfoComponents.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 0106c8cffa..4ce4cc0f92 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -37,6 +37,7 @@ Item text: catalog.i18nc("@action:button", "Connect") style: UM.Theme.styles.sidebar_action_button onClicked: connectActionDialog.show() + enabled: true visible: !base.printerConnected } } From 5c4f79eab3804cb1c0c1565e4a0d80db3aaa39e1 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Sep 2016 11:03:51 +0200 Subject: [PATCH 222/438] Add stub button to sync configuration from printer to Cura inb4 string-freeze CURA-2276 --- UM3InfoComponents.qml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 0106c8cffa..c51b33e688 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -64,12 +64,18 @@ Item visible: isUM3 Button { - height: UM.Theme.getSize("save_button_save_to_button").height tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") text: catalog.i18nc("@action:button", "Request Access") onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() visible: base.printerConnected && !base.printerAcceptsCommands } + + Button + { + tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") + text: catalog.i18nc("@action:button", "Activate Configuration") + visible: false + } } UM.I18nCatalog{id: catalog; name:"cura"} From cb05bbadd0b037e23ee3d91a966eeb0d7d3f2ce7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 13 Sep 2016 11:05:09 +0200 Subject: [PATCH 223/438] Explicitly set it to bool to prevent warnings --- DiscoverUM3Action.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index a33ddd31fe..c98bb13334 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -165,7 +165,7 @@ Cura.MachineAction Column { width: parent.width * 0.5 - visible: base.selectedPrinter + visible: base.selectedPrinter ? true : false spacing: UM.Theme.getSize("default_margin").height Label { @@ -220,7 +220,7 @@ Cura.MachineAction Button { text: catalog.i18nc("@action:button", "Connect") - enabled: base.selectedPrinter + enabled: base.selectedPrinter ? true : false onClicked: connectToPrinter() } } From 1cf92d432d709fbf2880c61fabb8ffecbcb5031f Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 13 Sep 2016 11:46:55 +0200 Subject: [PATCH 224/438] Typo. CURA-2277 --- UM3InfoComponents.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 0106c8cffa..d3f4818ccd 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -18,7 +18,7 @@ Item { objectName: "networkPrinterConnectButton" visible: isUM3 - spacing: UM.Theme.getSize("default_marging").width + spacing: UM.Theme.getSize("default_margin").width Button { From 400401929d7dd224ef58b6729046faf6c97d7afa Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 13 Sep 2016 14:36:36 +0200 Subject: [PATCH 225/438] User message now also states what the mismatch between cura and printer is CURA-2285 --- NetworkPrinterOutputDevice.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 921a704d46..612f2932c7 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -462,18 +462,25 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): extruder_manager = cura.Settings.ExtruderManager.getInstance() if print_information.materialLengths[index] != 0: variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"}) + core_name = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] if variant: - if variant.getName() != self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"]: - Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"], variant.getName()) - warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore selected for extruder {0}".format(index + 1))) + if variant.getName() != core_name: + Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName()) + warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1))) material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) if material: - if material.getMetaDataEntry("GUID") != self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"]: + remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] + if material.getMetaDataEntry("GUID") != remote_material_guid: Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, - self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"], + remote_material_guid, material.getMetaDataEntry("GUID")) - warnings.append(i18n_catalog.i18nc("@label", "Different material selected for extruder {0}").format(index + 1)) + + remote_materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True) + remote_material_name = "Unknown" + if remote_materials: + remote_material_name = remote_materials[0].getName() + warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1)) if warnings: text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") From a754952f5ef0f1c56121416c48d1247e8cf9a670 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 13 Sep 2016 17:43:57 +0200 Subject: [PATCH 226/438] Close connection now also ensures that cached machine & material ids are reset CURA-2354 --- NetworkPrinterOutputDevice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 612f2932c7..291dedab1a 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -400,6 +400,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_failed_message.hide() self._authentication_succeeded_message.hide() + # Reset stored material & hotend data. + self._material_ids = [""] * self._num_extruders + self._hotend_ids = [""] * self._num_extruders + if self._error_message: self._error_message.hide() From cc38705a6846a72b6e79987fe78e9bca6b0f7686 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 14 Sep 2016 12:55:11 +0200 Subject: [PATCH 227/438] Use right extruder for material --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 291dedab1a..fb15428cf1 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -472,7 +472,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName()) warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1))) - material = extruder_manager.getExtruderStack(0).findContainer({"type": "material"}) + material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"}) if material: remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] if material.getMetaDataEntry("GUID") != remote_material_guid: From 15d933ed73aba36e03ef120e9f53b456b7711a5a Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 14 Sep 2016 15:00:41 +0200 Subject: [PATCH 228/438] Show Extruders, PrintCores, Materials on Printer preference pane CURA-2276 --- UM3InfoComponents.qml | 63 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 9997978c79..55ddf7b98e 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -59,23 +59,66 @@ Item } - Item + Column { objectName: "networkPrinterConnectionInfo" visible: isUM3 - Button + spacing: UM.Theme.getSize("default_margin").width + anchors.fill: parent + + Row { - tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") - text: catalog.i18nc("@action:button", "Request Access") - onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() - visible: base.printerConnected && !base.printerAcceptsCommands + visible: base.printerConnected + spacing: UM.Theme.getSize("default_margin").width + + anchors.left: parent.left + anchors.right: parent.right + height: childrenRect.height + + Column + { + Repeater + { + model: Cura.ExtrudersModel { simpleNames: true } + Label { text: model.name } + } + } + Column + { + Repeater + { + id: nozzleColumn + model: Cura.MachineManager.printerOutputDevices[0].hotendIds + Label { text: nozzleColumn.model[index] } + } + } + Column + { + Repeater + { + id: materialColumn + model: Cura.MachineManager.printerOutputDevices[0].materialNames + Label { text: materialColumn.model[index] } + } + } } - Button + Row { - tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") - text: catalog.i18nc("@action:button", "Activate Configuration") - visible: false + Button + { + tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") + text: catalog.i18nc("@action:button", "Activate Configuration") + visible: base.printerConnected && false + } + + Button + { + tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") + text: catalog.i18nc("@action:button", "Request Access") + onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() + visible: base.printerConnected && !base.printerAcceptsCommands + } } } From f11308e74e86835212258c9fcfd9829618e4263f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 14 Sep 2016 16:21:00 +0200 Subject: [PATCH 229/438] Allow syncing materials & printcores from the Printers preference pane CURA-2276 --- DiscoverUM3Action.py | 10 ++++++++++ UM3InfoComponents.qml | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 747b568e25..845b7eb4a0 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -81,6 +81,16 @@ class DiscoverUM3Action(MachineAction): return "" + @pyqtSlot() + def loadConfigurationFromPrinter(self): + machine_manager = Application.getInstance().getMachineManager() + hotendIds = machine_manager.printerOutputDevices[0].hotendIds + for index in range(len(hotendIds)): + machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotendIds[index]) + materialIds = machine_manager.printerOutputDevices[0].materialIds + for index in range(len(materialIds)): + machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, materialIds[index]) + def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for UM3.") diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 55ddf7b98e..67e52aa3ab 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -109,7 +109,8 @@ Item { tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") text: catalog.i18nc("@action:button", "Activate Configuration") - visible: base.printerConnected && false + visible: base.printerConnected + onClicked: manager.loadConfigurationFromPrinter() } Button From 20f76b62fefb558615521a911d66535d3def0313 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 14 Sep 2016 16:40:20 +0200 Subject: [PATCH 230/438] Hide "Request Access" buttons when access request is active CURA-2277 & CURA-2276 --- NetworkPrinterOutputDevice.py | 10 +++++++++- UM3InfoComponents.qml | 32 +++++++++++++++----------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index fb15428cf1..2fef90ac86 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -242,7 +242,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_timer.stop() self._authentication_counter = 0 - self._authentication_state = auth_state + if auth_state != self._authentication_state: + self._authentication_state = auth_state + self.authenticationStateChanged.emit() + + authenticationStateChanged = pyqtSignal() + + @pyqtProperty(int, notify = authenticationStateChanged) + def authenticationState(self): + return self._authentication_state @pyqtSlot() def requestAuthentication(self, message_id = None, action_id = "Retry"): diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 67e52aa3ab..686a13e2cb 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -13,6 +13,7 @@ Item property bool isUM3: Cura.MachineManager.activeDefinitionId == "ultimaker3" property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands + property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested Row { @@ -27,7 +28,7 @@ Item text: catalog.i18nc("@action:button", "Request Access") style: UM.Theme.styles.sidebar_action_button onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() - visible: base.printerConnected && !base.printerAcceptsCommands + visible: !base.printerAcceptsCommands && !base.authenticationRequested } Button @@ -66,6 +67,14 @@ Item spacing: UM.Theme.getSize("default_margin").width anchors.fill: parent + Button + { + tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") + text: catalog.i18nc("@action:button", "Request Access") + onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() + visible: !base.printerAcceptsCommands && !base.authenticationRequested + } + Row { visible: base.printerConnected @@ -103,23 +112,12 @@ Item } } - Row + Button { - Button - { - tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") - text: catalog.i18nc("@action:button", "Activate Configuration") - visible: base.printerConnected - onClicked: manager.loadConfigurationFromPrinter() - } - - Button - { - tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") - text: catalog.i18nc("@action:button", "Request Access") - onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() - visible: base.printerConnected && !base.printerAcceptsCommands - } + tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") + text: catalog.i18nc("@action:button", "Activate Configuration") + visible: base.printerConnected + onClicked: manager.loadConfigurationFromPrinter() } } From 34aeb50b6a29e138a77df401d1cf48c23a3bd318 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Sep 2016 10:21:32 +0200 Subject: [PATCH 231/438] self._code_style --- DiscoverUM3Action.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 845b7eb4a0..8f4afb36ab 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -84,12 +84,12 @@ class DiscoverUM3Action(MachineAction): @pyqtSlot() def loadConfigurationFromPrinter(self): machine_manager = Application.getInstance().getMachineManager() - hotendIds = machine_manager.printerOutputDevices[0].hotendIds - for index in range(len(hotendIds)): - machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotendIds[index]) - materialIds = machine_manager.printerOutputDevices[0].materialIds - for index in range(len(materialIds)): - machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, materialIds[index]) + hotend_ids = machine_manager.printerOutputDevices[0].hotendIds + for index in range(len(hotend_ids)): + machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotend_ids[index]) + material_ids = machine_manager.printerOutputDevices[0].materialIds + for index in range(len(material_ids)): + machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, material_ids[index]) def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for UM3.") From 96e36c0555e0869b5da76df3612bc98bd6ed3efd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 15 Sep 2016 13:32:13 +0200 Subject: [PATCH 232/438] Refreshing zeroconf multiple times no longer causes cura crash CURA-2393 --- DiscoverUM3Action.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 8f4afb36ab..82d9240819 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -9,6 +9,8 @@ from PyQt5.QtQml import QQmlComponent, QQmlContext import os.path +import time + from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -25,6 +27,9 @@ class DiscoverUM3Action(MachineAction): Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) + self._min_time_between_restart_discovery = 2 + self._time_since_last_discovery = time.time() - self._min_time_between_restart_discovery + printersChanged = pyqtSignal() @pyqtSlot() @@ -37,10 +42,17 @@ class DiscoverUM3Action(MachineAction): @pyqtSlot() def restartDiscovery(self): - if not self._network_plugin: - self.startDiscovery() - else: - self._network_plugin.startDiscovery() + # Ensure that there is a bit of time between refresh attempts. + # This is a work around for an issue with Qt 5.5.1 up to Qt 5.7 which can segfault if we do this too often. + # It's most likely that the QML engine is still creating delegates, where the python side already deleted or + # garbage collected the data. + # Whatever the case, waiting a bit ensures that it doesn't crash. + if time.time() - self._time_since_last_discovery > self._min_time_between_restart_discovery: + if not self._network_plugin: + self.startDiscovery() + else: + self._network_plugin.startDiscovery() + self._time_since_last_discovery = time.time() def _onPrinterDiscoveryChanged(self, *args): self.printersChanged.emit() From 00c328bfe9f344aa1b6f5695d172c002734fbc59 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Sep 2016 13:44:12 +0200 Subject: [PATCH 233/438] Fix qml warnings when no printer is connected --- UM3InfoComponents.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 686a13e2cb..f0da9e627c 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -97,7 +97,7 @@ Item Repeater { id: nozzleColumn - model: Cura.MachineManager.printerOutputDevices[0].hotendIds + model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].hotendIds : null Label { text: nozzleColumn.model[index] } } } @@ -106,7 +106,7 @@ Item Repeater { id: materialColumn - model: Cura.MachineManager.printerOutputDevices[0].materialNames + model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].materialNames : null Label { text: materialColumn.model[index] } } } From cf2c3e79cfb3c8a06d4a2c3081a2bf00c63ca98b Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Sep 2016 14:17:04 +0200 Subject: [PATCH 234/438] Do not show the "Request Access" button when no printer is connected CURA-2277 --- UM3InfoComponents.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index f0da9e627c..598e26a7fd 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -28,7 +28,7 @@ Item text: catalog.i18nc("@action:button", "Request Access") style: UM.Theme.styles.sidebar_action_button onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() - visible: !base.printerAcceptsCommands && !base.authenticationRequested + visible: printerConnected && !base.printerAcceptsCommands && !base.authenticationRequested } Button From 2725abd4f43a44492969e1476ef98fd2448e8138 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Sep 2016 14:49:15 +0200 Subject: [PATCH 235/438] Don't show Request Access button when no printer is connected CURA-2276 --- UM3InfoComponents.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index 598e26a7fd..b5a707b280 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -38,7 +38,6 @@ Item text: catalog.i18nc("@action:button", "Connect") style: UM.Theme.styles.sidebar_action_button onClicked: connectActionDialog.show() - enabled: true visible: !base.printerConnected } } @@ -72,7 +71,7 @@ Item tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") text: catalog.i18nc("@action:button", "Request Access") onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() - visible: !base.printerAcceptsCommands && !base.authenticationRequested + visible: base.printerConnected && !base.printerAcceptsCommands && !base.authenticationRequested } Row From 5024581e2e79a5c3e590363ce6c689ffe5250479 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Sep 2016 17:42:36 +0200 Subject: [PATCH 236/438] Fix events for added components Not sharing the _context variable with the machine action fixes signals and events being sent to the additional components. CURA-2277 and CURA-2276 --- DiscoverUM3Action.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 82d9240819..0b0e61d585 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -21,7 +21,7 @@ class DiscoverUM3Action(MachineAction): self._network_plugin = None - self._context = None + self._additional_components_context = None self._additional_component = None self._additional_components_view = None @@ -110,9 +110,9 @@ class DiscoverUM3Action(MachineAction): self._additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) - self._context = QQmlContext(Application.getInstance()._engine.rootContext()) - self._context.setContextProperty("manager", self) - self._additional_components_view = self._additional_component.create(self._context) + self._additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) + self._additional_components_context.setContextProperty("manager", self) + self._additional_components_view = self._additional_component.create(self._additional_components_context) Application.getInstance().addAdditionalComponent("monitorButtons", self._additional_components_view.findChild(QObject, "networkPrinterConnectButton")) Application.getInstance().addAdditionalComponent("machinesDetailPane", self._additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) From c50cae6d28268fab23fe1e615165f449c1dba1aa Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Sep 2016 17:43:25 +0200 Subject: [PATCH 237/438] Fix showing additional components for UM3Ex CURA-2276 and CURA-2277 --- UM3InfoComponents.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/UM3InfoComponents.qml b/UM3InfoComponents.qml index b5a707b280..a5ed944773 100644 --- a/UM3InfoComponents.qml +++ b/UM3InfoComponents.qml @@ -10,7 +10,7 @@ Item { id: base - property bool isUM3: Cura.MachineManager.activeDefinitionId == "ultimaker3" + property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3" property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested @@ -28,7 +28,7 @@ Item text: catalog.i18nc("@action:button", "Request Access") style: UM.Theme.styles.sidebar_action_button onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() - visible: printerConnected && !base.printerAcceptsCommands && !base.authenticationRequested + visible: printerConnected && !printerAcceptsCommands && !authenticationRequested } Button @@ -38,7 +38,7 @@ Item text: catalog.i18nc("@action:button", "Connect") style: UM.Theme.styles.sidebar_action_button onClicked: connectActionDialog.show() - visible: !base.printerConnected + visible: !printerConnected } } @@ -71,12 +71,12 @@ Item tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer") text: catalog.i18nc("@action:button", "Request Access") onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication() - visible: base.printerConnected && !base.printerAcceptsCommands && !base.authenticationRequested + visible: printerConnected && !printerAcceptsCommands && !authenticationRequested } Row { - visible: base.printerConnected + visible: printerConnected spacing: UM.Theme.getSize("default_margin").width anchors.left: parent.left @@ -115,7 +115,7 @@ Item { tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") text: catalog.i18nc("@action:button", "Activate Configuration") - visible: base.printerConnected + visible: printerConnected onClicked: manager.loadConfigurationFromPrinter() } } From 7840f6a82e0ed488ac7c376a77475e4ed6aaff91 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 16 Sep 2016 16:19:20 +0200 Subject: [PATCH 238/438] Increased time to 5 sec CURA-2393 --- DiscoverUM3Action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 82d9240819..641c4de9b5 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -27,7 +27,7 @@ class DiscoverUM3Action(MachineAction): Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) - self._min_time_between_restart_discovery = 2 + self._min_time_between_restart_discovery = 5 self._time_since_last_discovery = time.time() - self._min_time_between_restart_discovery printersChanged = pyqtSignal() From 24cd26b37ec9822a6fad3083ea1864be6ad43479 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 19 Sep 2016 08:40:29 +0200 Subject: [PATCH 239/438] Show mjpg stream instead of slideshow CURA-2411 --- NetworkPrinterOutputDevice.py | 47 +++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2fef90ac86..ecb493d85d 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -113,6 +113,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._camera_timer.setSingleShot(False) self._camera_timer.timeout.connect(self._update_camera) + self._image_request = None + self._image_reply = None + + self._use_stream = True + self._stream_buffer = b"" + self._stream_buffer_start_index = -1 + self._camera_image_id = 0 self._authentication_counter = 0 @@ -192,6 +199,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def ipAddress(self): return self._address + def _start_camera_stream(self): + ## Request new image + url = QUrl("http://" + self._address + ":8080/?action=stream") + self._image_request = QNetworkRequest(url) + self._image_reply = self._manager.get(self._image_request) + self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) + def _update_camera(self): if not self._manager.networkAccessible(): return @@ -423,6 +437,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.stop() self._camera_timer.stop() + if self._image_reply: + self._image_reply.abort() + self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) + self._image_reply = None + self._image_request = None + def requestWrite(self, node, file_name = None, filter_by_machine = False): if self._progress != 0: self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to start a new print job because the printer is busy. Please check the printer.")) @@ -531,7 +551,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. - self._update_camera() + if not self._use_stream: + self._update_camera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) ## Check if this machine was authenticated before. @@ -539,7 +560,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_key", None) self._update_timer.start() - self._camera_timer.start() + if self._use_stream: + self._start_camera_stream() + else: + self._camera_timer.start() ## Stop requesting data from printer def disconnect(self): @@ -841,6 +865,25 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): else: Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) + def _onStreamDownloadProgress(self, bytes_received, bytes_total): + # An MJPG stream is (for our purpose) a stream of concatenated JPG images. + # JPG images start with the marker 0xFFD8, and end with 0xFFD9 + self._stream_buffer += self._image_reply.readAll() + + if self._stream_buffer_start_index == -1: + self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8') + stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9') + # If this happens to be more than a single frame, then so be it; the JPG decoder will + # ignore the extra data. We do it like this in order not to get a buildup of frames + + if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1: + jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2] + self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:] + self._stream_buffer_start_index = -1 + + self._camera_image.loadFromData(jpg_data) + self.newImage.emit() + def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 From d980f6b792941f08cd1c7db0d611806b2eacfead Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 19 Sep 2016 10:26:25 +0200 Subject: [PATCH 240/438] Code style CURA-2277/CURA-2276 --- DiscoverUM3Action.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index b940765de0..917824eccf 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -21,9 +21,9 @@ class DiscoverUM3Action(MachineAction): self._network_plugin = None - self._additional_components_context = None - self._additional_component = None - self._additional_components_view = None + self.__additional_components_context = None + self.__additional_component = None + self.__additional_components_view = None Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) @@ -107,12 +107,12 @@ class DiscoverUM3Action(MachineAction): Logger.log("d", "Creating additional ui components for UM3.") path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("JediWifiPrintingPlugin"), "UM3InfoComponents.qml")) - self._additional_component = QQmlComponent(Application.getInstance()._engine, path) + self.__additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) - self._additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) - self._additional_components_context.setContextProperty("manager", self) - self._additional_components_view = self._additional_component.create(self._additional_components_context) + self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) + self.__additional_components_context.setContextProperty("manager", self) + self.__additional_components_view = self.__additional_component.create(self.__additional_components_context) - Application.getInstance().addAdditionalComponent("monitorButtons", self._additional_components_view.findChild(QObject, "networkPrinterConnectButton")) - Application.getInstance().addAdditionalComponent("machinesDetailPane", self._additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) + Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton")) + Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) From e388b8126b02d28f5abadf0b2a51c0d6a3983a8b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 19 Sep 2016 11:29:57 +0200 Subject: [PATCH 241/438] No longer re-create zeroConf object, as this crashes CURA-2393 --- DiscoverUM3Action.py | 2 +- NetworkPrinterOutputDevicePlugin.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index b940765de0..2995c99f5d 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -27,7 +27,7 @@ class DiscoverUM3Action(MachineAction): Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) - self._min_time_between_restart_discovery = 5 + self._min_time_between_restart_discovery = 1 self._time_since_last_discovery = time.time() - self._min_time_between_restart_discovery printersChanged = pyqtSignal() diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index bd930493be..edcd406fab 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -41,7 +41,6 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._browser = None self._old_printers = [printer_name for printer_name in self._printers] self._printers = {} - self._zero_conf.__init__() self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) From 66b22134ba06a1c9fabb81f201a9eb0c456d1e6e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 19 Sep 2016 11:31:20 +0200 Subject: [PATCH 242/438] Always hide progress message if connection is lost --- NetworkPrinterOutputDevice.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2fef90ac86..ddee244355 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -294,6 +294,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the network was lost.")) self._connection_message.show() + + if self._progress_message: + self._progress_message.hide() + # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: @@ -305,7 +309,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() - self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return @@ -321,6 +324,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your printer to see if it is connected.")) self._connection_message.show() + + if self._progress_message: + self._progress_message.hide() + # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: @@ -332,7 +339,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() - self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. self.setConnectionState(ConnectionState.error) From 7da05e0f36137a88bf582f341508bbe5c45dd652 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 19 Sep 2016 13:38:52 +0200 Subject: [PATCH 243/438] Post reply is now set to None after abort CURA-2295 --- NetworkPrinterOutputDevice.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index ddee244355..3d9a96d8ad 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -297,7 +297,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._progress_message: self._progress_message.hide() - + # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: @@ -309,6 +309,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() + self._post_reply = None except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return @@ -339,6 +340,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() + self._post_reply = None except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. self.setConnectionState(ConnectionState.error) From 400360cb34e5efc699d14823fc70090828d90939 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 19 Sep 2016 15:05:33 +0200 Subject: [PATCH 244/438] Add check if additional components were successfully created --- DiscoverUM3Action.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 854e40e387..0d8c0ad060 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -112,7 +112,11 @@ class DiscoverUM3Action(MachineAction): # We need access to engine (although technically we can't) self.__additional_components_context = QQmlContext(Application.getInstance()._engine.rootContext()) self.__additional_components_context.setContextProperty("manager", self) + self.__additional_components_view = self.__additional_component.create(self.__additional_components_context) + if not self.__additional_components_view: + Logger.log("w", "Could not create ui components for UM3.") + return Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton")) Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) From f75e8c9537abc6ddf9d2526472453bb8e4205cc8 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 20 Sep 2016 11:53:14 +0200 Subject: [PATCH 245/438] Add version number --- __init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/__init__.py b/__init__.py index 6b7457ad36..be9f1195ec 100644 --- a/__init__.py +++ b/__init__.py @@ -11,6 +11,7 @@ def getMetaData(): "name": "UM3 Network Connection", "author": "Ultimaker", "description": catalog.i18nc("@info:whatsthis", "Manages network connections to Ultimaker 3 printers"), + "version": "1.0", "api": 3 } } From df9f940ff44dd13bc752a347a8123fe7756a64fc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 20 Sep 2016 17:45:55 +0200 Subject: [PATCH 246/438] Added cancel button to upload print CURA-2384 --- NetworkPrinterOutputDevice.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 3d9a96d8ad..cb8ddde82e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -151,6 +151,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._last_command = "" + self._compressing_print = False + def _onNetworkAccesibleChanged(self, accessible): Logger.log("d", "Network accessible state changed to: %s", accessible) @@ -585,6 +587,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return user return "Unknown User" # Couldn't find out username. + def _progressMessageActionTrigger(self, message_id = None, action_id="abort"): + if action_id == "abort": + Logger.log("d", "User aborted sending print to remote.") + self._progress_message.hide() + self._compressing_print = False + if self._post_reply: + self._post_reply.abort() + ## Attempt to start a new print. # This function can fail to actually start a print due to not being authenticated or another print already # being in progress. @@ -592,12 +602,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): try: self._send_gcode_start = time() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) + self._progress_message.addAction("abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") + self._progress_message.actionTriggered.connect(self._progressMessageActionTrigger) self._progress_message.show() Logger.log("d", "Started sending g-code to remote printer.") - + self._compressing_print = True ## Mash the data into single string byte_array_file_data = b"" for line in self._gcode: + if not self._compressing_print: + self._progress_message.hide() + return # Stop trying to zip, abort was called. if self._use_gzip: byte_array_file_data += gzip.compress(line.encode("utf-8")) QCoreApplication.processEvents() # Ensure that the GUI does not freeze. @@ -611,6 +626,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): else: file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName + self._compressing_print = False ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) From ba53b0109bcbdb1eba7f7857a8f4af7ceea96f04 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 21 Sep 2016 14:00:11 +0200 Subject: [PATCH 247/438] Fix connecting to the selected printer when pressing "Finish" in add machine wizard --- DiscoverUM3Action.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index c98bb13334..8b20066ab2 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -20,7 +20,7 @@ Cura.MachineAction onNextClicked: { // Connect to the printer if the MachineAction is currently shown - if(base.parent == dialog) + if(base.parent.wizard == dialog) { connectToPrinter(); } From d8f0b634aff9a600b7678219b6eb3b91c709e0bf Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 21 Sep 2016 14:01:05 +0200 Subject: [PATCH 248/438] Switch back to Print Setup when canceling sending a print to the printer CURA-2384 --- NetworkPrinterOutputDevice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index cb8ddde82e..e213684d56 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -587,13 +587,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): return user return "Unknown User" # Couldn't find out username. - def _progressMessageActionTrigger(self, message_id = None, action_id="abort"): - if action_id == "abort": + def _progressMessageActionTrigger(self, message_id = None, action_id = None): + if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_print = False if self._post_reply: self._post_reply.abort() + Application.getInstance().showPrintMonitor.emit(False) ## Attempt to start a new print. # This function can fail to actually start a print due to not being authenticated or another print already @@ -602,7 +603,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): try: self._send_gcode_start = time() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) - self._progress_message.addAction("abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") + self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._progressMessageActionTrigger) self._progress_message.show() Logger.log("d", "Started sending g-code to remote printer.") From 24827ba5a1e47c7a28b85c5f5b8d005dab2bbf34 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 21 Sep 2016 15:59:26 +0200 Subject: [PATCH 249/438] Added more logging when authentication was denied CURA-2270 --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index cb8ddde82e..d6b7ff22e4 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -801,6 +801,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif status_code == 403: # If we already had an auth (eg; didn't request one), we only need a single 403 to see it as denied. if self._authentication_state != AuthState.AuthenticationRequested: + Logger.log("d", "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s", self._authentication_state) self.setAuthenticationState(AuthState.AuthenticationDenied) elif status_code == 200: self.setAuthenticationState(AuthState.Authenticated) From bc76744ddb5f926950272d2368981eea0d1d8634 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 21 Sep 2016 16:40:42 +0200 Subject: [PATCH 250/438] Add (more) logging to authentication denied state CURA-2270 --- NetworkPrinterOutputDevice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 7c0e78d0bb..cc46c2a3a5 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -227,14 +227,15 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Once we are authenticated we need to send all material profiles. self.sendMaterialProfiles() elif auth_state == AuthState.AuthenticationDenied: - Logger.log("d", "Authentication state changed to authentication denied") self.setAcceptsCommands(False) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network to {0}. No access to control the printer.").format(self.name)) self._authentication_requested_message.hide() if self._authentication_request_active: if self._authentication_timer.remainingTime() > 0: + Logger.logException("d", "Authentication state changed to authentication denied before the request timeout.") self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request was denied on the printer.")) else: + Logger.logException("d", "Authentication state changed to authentication denied due to a timeout") self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request failed due to a timeout.")) self._authentication_failed_message.show() From 64571bff0b8fb5daba0ea35c18af87d73c732f85 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 21 Sep 2016 16:42:02 +0200 Subject: [PATCH 251/438] Revert exception logging --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index cc46c2a3a5..2ec9e1bd92 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -232,10 +232,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_requested_message.hide() if self._authentication_request_active: if self._authentication_timer.remainingTime() > 0: - Logger.logException("d", "Authentication state changed to authentication denied before the request timeout.") + Logger.log("d", "Authentication state changed to authentication denied before the request timeout.") self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request was denied on the printer.")) else: - Logger.logException("d", "Authentication state changed to authentication denied due to a timeout") + Logger.log("d", "Authentication state changed to authentication denied due to a timeout") self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request failed due to a timeout.")) self._authentication_failed_message.show() From c0839bcfbbe12c77b33f1d9a374651117042af6a Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 23 Sep 2016 16:50:57 +0200 Subject: [PATCH 252/438] Correct and format the documentation example json --- NetworkPrinterOutputDevice.py | 51 ++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2ec9e1bd92..1a3e0db480 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -49,25 +49,38 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # This holds the full JSON file that was received from the last request. # The JSON looks like: - # {'led': {'saturation': 0.0, 'brightness': 100.0, 'hue': 0.0}, - # 'beep': {}, 'network': {'wifi_networks': [], - # 'ethernet': {'connected': True, 'enabled': True}, - # 'wifi': {'ssid': 'xxxx', 'connected': False, 'enabled': False}}, - # 'diagnostics': {}, - # 'bed': {'temperature': {'target': 60.0, 'current': 44.4}}, - # 'heads': [{'max_speed': {'z': 40.0, 'y': 300.0, 'x': 300.0}, - # 'position': {'z': 20.0, 'y': 6.0, 'x': 180.0}, - # 'fan': 0.0, - # 'jerk': {'z': 0.4, 'y': 20.0, 'x': 20.0}, - # 'extruders': [ - # {'feeder': {'max_speed': 45.0, 'jerk': 5.0, 'acceleration': 3000.0}, - # 'active_material': {'GUID': 'xxxxxxx', 'length_remaining': -1.0}, - # 'hotend': {'temperature': {'target': 0.0, 'current': 22.8}, 'id': 'AA 0.4'}}, - # {'feeder': {'max_speed': 45.0, 'jerk': 5.0, 'acceleration': 3000.0}, - # 'active_material': {'GUID': 'xxxx', 'length_remaining': -1.0}, - # 'hotend': {'temperature': {'target': 0.0, 'current': 22.8}, 'id': 'BB 0.4'}}], - # 'acceleration': 3000.0}], - # 'status': 'printing'} + #{ + # "led": {"saturation": 0.0, "brightness": 100.0, "hue": 0.0}, + # "beep": {}, + # "network": { + # "wifi_networks": [], + # "ethernet": {"connected": true, "enabled": true}, + # "wifi": {"ssid": "xxxx", "connected": False, "enabled": False} + # }, + # "diagnostics": {}, + # "bed": {"temperature": {"target": 60.0, "current": 44.4}}, + # "heads": [{ + # "max_speed": {"z": 40.0, "y": 300.0, "x": 300.0}, + # "position": {"z": 20.0, "y": 6.0, "x": 180.0}, + # "fan": 0.0, + # "jerk": {"z": 0.4, "y": 20.0, "x": 20.0}, + # "extruders": [ + # { + # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, + # "active_material": {"GUID": "xxxxxxx", "length_remaining": -1.0}, + # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "AA 0.4"} + # }, + # { + # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, + # "active_material": {"GUID": "xxxx", "length_remaining": -1.0}, + # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "BB 0.4"} + # } + # ], + # "acceleration": 3000.0 + # }], + # "status": "printing" + #} + self._json_printer_state = {} ## Todo: Hardcoded value now; we should probably read this from the machine file. From b4ba7a64a98bb5a6514433f4cf6b9b49d9872c4f Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 26 Sep 2016 12:01:54 +0200 Subject: [PATCH 253/438] Only update printer list after printer is added/removed This makes it emit a signal only after addPrinter and removePrinter has completed executing, so we know that updating the list is done by the time it refreshes the list view in QML. Contributes to issue CURA-2393. --- DiscoverUM3Action.py | 3 +-- NetworkPrinterOutputDevicePlugin.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 0d8c0ad060..95105425c1 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -36,8 +36,7 @@ class DiscoverUM3Action(MachineAction): def startDiscovery(self): if not self._network_plugin: self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("JediWifiPrintingPlugin") - self._network_plugin.addPrinterSignal.connect(self._onPrinterDiscoveryChanged) - self._network_plugin.removePrinterSignal.connect(self._onPrinterDiscoveryChanged) + self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() @pyqtSlot() diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index edcd406fab..a168f2ae43 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -30,6 +30,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): addPrinterSignal = Signal() removePrinterSignal = Signal() + printerListChanged = Signal() ## Start looking for devices on network. def start(self): @@ -73,6 +74,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced? self._printers[printer.getKey()].connect() printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + self.printerListChanged.emit() def removePrinter(self, name): printer = self._printers.pop(name, None) @@ -80,6 +82,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if printer.isConnected(): printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged) printer.disconnect() + self.printerListChanged.emit() ## Handler for when the connection state of one of the detected printers changes def _onPrinterConnectionStateChanged(self, key): From 368851b9101b1b81f46d3b884ffb8f85c548afa8 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 26 Sep 2016 14:07:16 +0200 Subject: [PATCH 254/438] Check if a request was made recently before claiming a timeout occurred If the main thread locks up for longer than the network timeout period, the connection may incorrectly seem to have reached a timeout. CURA-2440 --- NetworkPrinterOutputDevice.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 1a3e0db480..acfc566b02 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -156,6 +156,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._connection_state_before_timeout = None self._last_response_time = time() + self._last_request_time = None self._response_timeout_time = 10 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 @@ -286,6 +287,10 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): time_since_last_response = time() - self._last_response_time else: time_since_last_response = 0 + if self._last_request_time: + time_since_last_request = time() - self._last_request_time + else: + time_since_last_request = 1000000 # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. @@ -334,8 +339,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state - if self._last_response_time and not self._connection_state_before_timeout: - if time_since_last_response > self._response_timeout_time: + if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: + if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: # Go into timeout state. Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accessible.", time_since_last_response) self._connection_state_before_timeout = self._connection_state @@ -377,6 +382,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): print_job_request = QNetworkRequest(url) self._manager.get(print_job_request) + self._last_request_time = time() + def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onFinished) From 720dd9c50cf85d511e05366f12636ccc85240558 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 26 Sep 2016 16:28:06 +0200 Subject: [PATCH 255/438] Use the semantically more correct infinity --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index acfc566b02..e2fe36f118 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -290,7 +290,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_request_time: time_since_last_request = time() - self._last_request_time else: - time_since_last_request = 1000000 # An irrelevantly large number of seconds + time_since_last_request = float("inf") # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. From fefa111b5aabb5f9951776d12b167996e6781fdf Mon Sep 17 00:00:00 2001 From: awhiemstra Date: Tue, 27 Sep 2016 15:52:31 +0200 Subject: [PATCH 256/438] Update references to JediWifiPrinting to UM3NetworkPrinting Contributes to CURA-2342 --- DiscoverUM3Action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 95105425c1..c33e6aed02 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -35,7 +35,7 @@ class DiscoverUM3Action(MachineAction): @pyqtSlot() def startDiscovery(self): if not self._network_plugin: - self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("JediWifiPrintingPlugin") + self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() @@ -105,7 +105,7 @@ class DiscoverUM3Action(MachineAction): def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for UM3.") - path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("JediWifiPrintingPlugin"), "UM3InfoComponents.qml")) + path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml")) self.__additional_component = QQmlComponent(Application.getInstance()._engine, path) # We need access to engine (although technically we can't) From 7408f31738434217455db842070a9d607b464cb2 Mon Sep 17 00:00:00 2001 From: awhiemstra Date: Tue, 27 Sep 2016 15:53:33 +0200 Subject: [PATCH 257/438] Change install location to UM3NetworkPrinting Contributes to CURA-2342 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c551e7f707..8e9db68ddf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -project(JediWifiPrintingPlugin NONE) +project(UM3NetworkPrinting NONE) cmake_minimum_required(VERSION 2.8.12) install(FILES @@ -10,5 +10,5 @@ install(FILES UM3InfoComponents.qml LICENSE README.md - DESTINATION lib/cura/plugins/JediWifiPrintingPlugin + DESTINATION lib/cura/plugins/UM3NetworkPrinting ) From fb371f55561f776683d357562d81e892d7866532 Mon Sep 17 00:00:00 2001 From: awhiemstra Date: Tue, 27 Sep 2016 15:57:01 +0200 Subject: [PATCH 258/438] Remove some more references to Jedi Contributes to CURA-2342 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 277111412c..0e88ce593b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# JediWifiPrintingPlugin -Secret plugin to enable wifi printing from Cura to JediPrinter +# UM3NetworkPrintingPlugin +Secret plugin to enable wifi printing from Cura to UM3 Intructions ---- -- Clone repo into [Cura installation folder]/plugins/JediWifiPrintingPlugin (Or somewhere else and add a link..) +- Clone repo into [Cura installation folder]/plugins/UM3NetworkPrinting (Or somewhere else and add a link..) - pip3 install python3-zeroconf From 1ed3fc7c3716a65f46ea063a22f314ccb38a82f6 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 28 Sep 2016 11:31:52 +0200 Subject: [PATCH 259/438] Don't reset zeroconf until after (most) printers have been found CURA-2060 --- DiscoverUM3Action.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index c33e6aed02..cda6ea386a 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -27,8 +27,8 @@ class DiscoverUM3Action(MachineAction): Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) - self._min_time_between_restart_discovery = 1 - self._time_since_last_discovery = time.time() - self._min_time_between_restart_discovery + self._last_zeroconf_event_time = time.time() + self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset printersChanged = pyqtSignal() @@ -41,19 +41,19 @@ class DiscoverUM3Action(MachineAction): @pyqtSlot() def restartDiscovery(self): - # Ensure that there is a bit of time between refresh attempts. + # Ensure that there is a bit of time after a printer has been discovered. # This is a work around for an issue with Qt 5.5.1 up to Qt 5.7 which can segfault if we do this too often. # It's most likely that the QML engine is still creating delegates, where the python side already deleted or # garbage collected the data. # Whatever the case, waiting a bit ensures that it doesn't crash. - if time.time() - self._time_since_last_discovery > self._min_time_between_restart_discovery: + if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period: if not self._network_plugin: self.startDiscovery() else: self._network_plugin.startDiscovery() - self._time_since_last_discovery = time.time() def _onPrinterDiscoveryChanged(self, *args): + self._last_zeroconf_event_time = time.time() self.printersChanged.emit() @pyqtProperty("QVariantList", notify = printersChanged) From da4ea2e45024bc811d079aec889ecd4d7dcd96a3 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 28 Sep 2016 16:36:09 +0200 Subject: [PATCH 260/438] Better refresh button: now emits signal and re-instantiates zeroconf. Signal emits let the UI display an empty list. Re-instantiation copes with network changes. Contributes to CURA-2372 --- DiscoverUM3Action.py | 1 - NetworkPrinterOutputDevicePlugin.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index cda6ea386a..6c6757d28b 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -104,7 +104,6 @@ class DiscoverUM3Action(MachineAction): def _createAdditionalComponentsView(self): Logger.log("d", "Creating additional ui components for UM3.") - path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml")) self.__additional_component = QQmlComponent(Application.getInstance()._engine, path) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index a168f2ae43..f0966c9fe3 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -15,7 +15,7 @@ import time class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def __init__(self): super().__init__() - self._zero_conf = Zeroconf() + self._zero_conf = None self._browser = None self._printers = {} @@ -37,17 +37,22 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.startDiscovery() def startDiscovery(self): + self.stop() if self._browser: self._browser.cancel() self._browser = None self._old_printers = [printer_name for printer_name in self._printers] self._printers = {} - + self.printerListChanged.emit() + # After network switching, one must make a new instance of Zeroconf + # On windows, the instance creation is very fast (unnoticable). Other platforms? + self._zero_conf = Zeroconf() self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) ## Stop looking for devices on network. def stop(self): - self._zero_conf.close() + if self._zero_conf is not None: + self._zero_conf.close() def getPrinters(self): return self._printers From 8e26d63390d9d537908475dc0c9ae53d96a1be43 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 28 Sep 2016 17:45:56 +0200 Subject: [PATCH 261/438] Get a list of ipadresses/hosts to check for UM3 printers... ...in case discovery does not work. This uses the API to look up info on the printer instead of relying on zeroconf. CURA-2483 --- NetworkPrinterOutputDevicePlugin.py | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index a168f2ae43..caf20c5629 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -5,6 +5,10 @@ from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.Application import Application +from UM.Preferences import Preferences + +from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply +from PyQt5.QtCore import QUrl import time @@ -19,6 +23,12 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._browser = None self._printers = {} + self._api_version = "1" + self._api_prefix = "/api/v" + self._api_version + "/" + + self._network_manager = QNetworkAccessManager() + self._network_manager.finished.connect(self._onNetworkRequestFinished) + # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces # authentication requests. self._old_printers = [] @@ -28,6 +38,11 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.removePrinterSignal.connect(self.removePrinter) Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) + # Get list of manual printers from preferences + preferences = Preferences.getInstance() + preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames + self._manual_instances = preferences.getValue("um3networkprinting/manual_instances").split(",") + addPrinterSignal = Signal() removePrinterSignal = Signal() printerListChanged = Signal() @@ -45,6 +60,28 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) + # Look for manual instances from preference + for address in self._manual_instances: + url = QUrl("http://" + address + self._api_prefix + "system/name") + + name_request = QNetworkRequest(url) + self._network_manager.get(name_request) + + ## Handler for all requests that have finished. + def _onNetworkRequestFinished(self, reply): + reply_url = reply.url().toString() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + if reply.operation() == QNetworkAccessManager.GetOperation: + if "system/name" in reply_url: # Name returned from printer. + if status_code == 200: + address = reply.url().host() + name = reply.readAll() + + instance_name = "manual:%s" % address + properties = { b"name": name.data() } + self.addPrinter(instance_name, address, properties) + ## Stop looking for devices on network. def stop(self): self._zero_conf.close() From 8dd8fd740c885eafb6ebdc3477cde2ce122cf99c Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 28 Sep 2016 23:05:53 +0200 Subject: [PATCH 262/438] Add UI for managing manually added printers CURA-2483 --- DiscoverUM3Action.qml | 98 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 8b20066ab2..4079df3f2e 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -66,13 +66,45 @@ Cura.MachineAction text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your Ultimaker 3, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your Ultimaker 3 from the list below:") } - Button + Row { - id: rediscoverButton - text: catalog.i18nc("@title", "Refresh") - onClicked: manager.restartDiscovery() - anchors.right: parent.right - anchors.rightMargin: parent.width * 0.5 + spacing: UM.Theme.getSize("default_lining").width + + Button + { + id: addButton + text: catalog.i18nc("@action:button", "Add"); + onClicked: + { + manualPrinterDialog.showDialog("", ""); + } + } + + Button + { + id: editButton + text: catalog.i18nc("@action:button", "Edit") + enabled: base.selectedPrinter && base.selectedPrinter.getKey().substr(0,7) =="manual:" + onClicked: + { + manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress); + } + } + + Button + { + id: removeButton + text: catalog.i18nc("@action:button", "Remove") + enabled: base.selectedPrinter && base.selectedPrinter.getKey().substr(0,7) =="manual:" + onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey()) + } + + Button + { + id: rediscoverButton + text: catalog.i18nc("@title", "Refresh") + onClicked: manager.restartDiscovery() + } } Row @@ -226,4 +258,58 @@ Cura.MachineAction } } } + + UM.Dialog + { + id: manualPrinterDialog + property string printerKey + property alias addressText: addressField.text + + title: catalog.i18nc("@label", "IP Address") + + minimumWidth: 400 * Screen.devicePixelRatio + minimumHeight: 120 * Screen.devicePixelRatio + width: minimumWidth + height: minimumHeight + + signal showDialog(string key, string address) + onShowDialog: + { + printerKey = key; + + addressText = address; + addressField.selectAll(); + addressField.focus = true; + + manualPrinterDialog.show(); + } + + onAccepted: + { + manager.setManualPrinter(printerKey, addressText) + } + + Column { + anchors.fill: parent + + TextField { + id: addressField + width: parent.width + maximumLength: 40 + } + } + + rightButtons: [ + Button { + text: catalog.i18nc("@action:button","Cancel") + onClicked: manualPrinterDialog.reject() + }, + Button { + text: catalog.i18nc("@action:button", "Ok") + onClicked: manualPrinterDialog.accept() + enabled: manualPrinterDialog.addressText.trim() != "" + isDefault: true + } + ] + } } \ No newline at end of file From eba49ee8c287c6b63fbe82859fbf83aacbf5afda Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 28 Sep 2016 23:08:23 +0200 Subject: [PATCH 263/438] Always show manual printer instances, even before they are validated CURA-2483 --- NetworkPrinterOutputDevicePlugin.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index caf20c5629..f1d5acefef 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -62,10 +62,20 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): # Look for manual instances from preference for address in self._manual_instances: - url = QUrl("http://" + address + self._api_prefix + "system/name") + self.addManualPrinter(address) - name_request = QNetworkRequest(url) - self._network_manager.get(name_request) + def addManualPrinter(self, address): + # Add a preliminary printer instance + name = address + instance_name = "manual:%s" % address + properties = { b"name": name.encode("UTF-8") } + self.addPrinter(instance_name, address, properties) + + # Check if a printer exists at this address + # If a printer responds, it will replace the preliminary printer created above + url = QUrl("http://" + address + self._api_prefix + "system/name") + name_request = QNetworkRequest(url) + self._network_manager.get(name_request) ## Handler for all requests that have finished. def _onNetworkRequestFinished(self, reply): @@ -76,10 +86,12 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if "system/name" in reply_url: # Name returned from printer. if status_code == 200: address = reply.url().host() - name = reply.readAll() + name = reply.readAll().data().decode() + name = ("%s (%s)" % (name, address)) instance_name = "manual:%s" % address - properties = { b"name": name.data() } + properties = { b"name": name.encode("UTF-8") } + self.removePrinter(instance_name) self.addPrinter(instance_name, address, properties) ## Stop looking for devices on network. From 578b4d3826fa8f886553561381f65f709a3d95d8 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Sep 2016 08:47:29 +0200 Subject: [PATCH 264/438] Implement Adding, Editing and Removing manual printers CURA-2384 --- DiscoverUM3Action.py | 16 +++++++++++++++ DiscoverUM3Action.qml | 2 +- NetworkPrinterOutputDevicePlugin.py | 32 ++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index cda6ea386a..35f4e48c71 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -52,6 +52,22 @@ class DiscoverUM3Action(MachineAction): else: self._network_plugin.startDiscovery() + @pyqtSlot(str, str) + def removeManualPrinter(self, key, address): + if not self._network_plugin: + return + + self._network_plugin.removeManualPrinter(key, address) + + @pyqtSlot(str, str) + def setManualPrinter(self, key, address): + if key != "": + # This manual printer replaces a current manual printer + self._network_plugin.removeManualPrinter(key) + + if address != "": + self._network_plugin.addManualPrinter(address) + def _onPrinterDiscoveryChanged(self, *args): self._last_zeroconf_event_time = time.time() self.printersChanged.emit() diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 4079df3f2e..0cd4ba0481 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -96,7 +96,7 @@ Cura.MachineAction id: removeButton text: catalog.i18nc("@action:button", "Remove") enabled: base.selectedPrinter && base.selectedPrinter.getKey().substr(0,7) =="manual:" - onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey()) + onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress) } Button diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index f1d5acefef..83416c4fc6 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -39,9 +39,9 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) # Get list of manual printers from preferences - preferences = Preferences.getInstance() - preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames - self._manual_instances = preferences.getValue("um3networkprinting/manual_instances").split(",") + self._preferences = Preferences.getInstance() + self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames + self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",") addPrinterSignal = Signal() removePrinterSignal = Signal() @@ -65,11 +65,17 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.addManualPrinter(address) def addManualPrinter(self, address): - # Add a preliminary printer instance + if address not in self._manual_instances: + self._manual_instances.append(address) + self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) + name = address instance_name = "manual:%s" % address properties = { b"name": name.encode("UTF-8") } - self.addPrinter(instance_name, address, properties) + + if instance_name not in self._printers: + # Add a preliminary printer instance + self.addPrinter(instance_name, address, properties) # Check if a printer exists at this address # If a printer responds, it will replace the preliminary printer created above @@ -77,6 +83,16 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): name_request = QNetworkRequest(url) self._network_manager.get(name_request) + def removeManualPrinter(self, key, address = None): + if key in self._printers: + if not address: + address = self._printers[key].ipAddress + self.removePrinter(key) + + if address in self._manual_instances: + self._manual_instances.remove(address) + self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) + ## Handler for all requests that have finished. def _onNetworkRequestFinished(self, reply): reply_url = reply.url().toString() @@ -91,8 +107,10 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): instance_name = "manual:%s" % address properties = { b"name": name.encode("UTF-8") } - self.removePrinter(instance_name) - self.addPrinter(instance_name, address, properties) + if instance_name in self._printers: + # Only replace the printer if it is still in the list of (manual) printers + self.removePrinter(instance_name) + self.addPrinter(instance_name, address, properties) ## Stop looking for devices on network. def stop(self): From a2722c757173dbb48b7c10012f49803efad3e2fc Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Sep 2016 09:41:21 +0200 Subject: [PATCH 265/438] Get firmware version through API... ...for parity between discovered printers and manual printers CURA-2384 --- NetworkPrinterOutputDevicePlugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 83416c4fc6..96c43d5d0d 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -11,6 +11,7 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkRepl from PyQt5.QtCore import QUrl import time +import json ## This plugin handles the connection detection & creation of output device objects for the UM3 printer. # Zero-Conf is used to detect printers, which are saved in a dict. @@ -79,7 +80,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): # Check if a printer exists at this address # If a printer responds, it will replace the preliminary printer created above - url = QUrl("http://" + address + self._api_prefix + "system/name") + url = QUrl("http://" + address + self._api_prefix + "system") name_request = QNetworkRequest(url) self._network_manager.get(name_request) @@ -99,14 +100,14 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: - if "system/name" in reply_url: # Name returned from printer. + if "system" in reply_url: # Name returned from printer. if status_code == 200: + system_info = json.loads(bytes(reply.readAll()).decode("utf-8")) address = reply.url().host() - name = reply.readAll().data().decode() - name = ("%s (%s)" % (name, address)) + name = ("%s (%s)" % (system_info["name"], address)) instance_name = "manual:%s" % address - properties = { b"name": name.encode("UTF-8") } + properties = { b"name": name.encode("UTF-8"), b"firmware_version": system_info["firmware"].encode("UTF-8") } if instance_name in self._printers: # Only replace the printer if it is still in the list of (manual) printers self.removePrinter(instance_name) From ea9ba87fa4e96924d1b55ba761823e38a0b29541 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Sep 2016 14:29:33 +0200 Subject: [PATCH 266/438] Only allow connecting if the printer has responded to API query CURA-2483 --- DiscoverUM3Action.qml | 14 ++++++++++---- NetworkPrinterOutputDevicePlugin.py | 18 +++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 0cd4ba0481..4daeaa1c37 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -11,6 +11,7 @@ Cura.MachineAction id: base anchors.fill: parent; property var selectedPrinter: null + property bool completeProperties: true property var connectingToPrinter: null Connections @@ -84,7 +85,7 @@ Cura.MachineAction { id: editButton text: catalog.i18nc("@action:button", "Edit") - enabled: base.selectedPrinter && base.selectedPrinter.getKey().substr(0,7) =="manual:" + enabled: base.selectedPrinter != null && (base.selectedPrinter.getKey().substr(0,7) =="manual:") onClicked: { manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress); @@ -95,7 +96,7 @@ Cura.MachineAction { id: removeButton text: catalog.i18nc("@action:button", "Remove") - enabled: base.selectedPrinter && base.selectedPrinter.getKey().substr(0,7) =="manual:" + enabled: base.selectedPrinter != null && (base.selectedPrinter.getKey().substr(0,7) =="manual:") onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress) } @@ -150,7 +151,12 @@ Cura.MachineAction } width: parent.width currentIndex: -1 - onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] + onCurrentIndexChanged: + { + base.selectedPrinter = listview.model[currentIndex]; + // Only allow connecting if the printer has responded to API query since the last refresh + base.completeProperties = base.selectedPrinter != null && (base.selectedPrinter.firmwareVersion != ""); + } Component.onCompleted: manager.startDiscovery() delegate: Rectangle { @@ -252,7 +258,7 @@ Cura.MachineAction Button { text: catalog.i18nc("@action:button", "Connect") - enabled: base.selectedPrinter ? true : false + enabled: (base.selectedPrinter && base.completeProperties) ? true : false onClicked: connectToPrinter() } } diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 96c43d5d0d..4843ab8975 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -63,7 +63,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): # Look for manual instances from preference for address in self._manual_instances: - self.addManualPrinter(address) + if address: + self.addManualPrinter(address) def addManualPrinter(self, address): if address not in self._manual_instances: @@ -72,17 +73,13 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): name = address instance_name = "manual:%s" % address - properties = { b"name": name.encode("UTF-8") } + properties = { b"name": name.encode("UTF-8"), b"incomplete": True } if instance_name not in self._printers: # Add a preliminary printer instance self.addPrinter(instance_name, address, properties) - # Check if a printer exists at this address - # If a printer responds, it will replace the preliminary printer created above - url = QUrl("http://" + address + self._api_prefix + "system") - name_request = QNetworkRequest(url) - self._network_manager.get(name_request) + self.checkManualPrinter(address) def removeManualPrinter(self, key, address = None): if key in self._printers: @@ -94,6 +91,13 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._manual_instances.remove(address) self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) + def checkManualPrinter(self, address): + # Check if a printer exists at this address + # If a printer responds, it will replace the preliminary printer created above + url = QUrl("http://" + address + self._api_prefix + "system") + name_request = QNetworkRequest(url) + self._network_manager.get(name_request) + ## Handler for all requests that have finished. def _onNetworkRequestFinished(self, reply): reply_url = reply.url().toString() From b25b92a81bce184b406e3ebdc7feef3072a25e32 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Sep 2016 14:46:31 +0200 Subject: [PATCH 267/438] Don't allow spaces, commas and other illegal chars in the address CURA-2384 --- DiscoverUM3Action.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 4daeaa1c37..5bac7d89ba 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -302,6 +302,10 @@ Cura.MachineAction id: addressField width: parent.width maximumLength: 40 + validator: RegExpValidator + { + regExp: /[a-zA-Z0-9\.\-\_]*/ + } } } From 042ebe76ba4c240fcc8bdb1753cb780363f5fbb5 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Sep 2016 15:32:42 +0200 Subject: [PATCH 268/438] Don't rely on key prefix, remove code duplication (_api_prefix) CURA-2384 --- DiscoverUM3Action.qml | 6 +++--- NetworkPrinterOutputDevice.py | 13 ++++++++++--- NetworkPrinterOutputDevicePlugin.py | 6 +++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 5bac7d89ba..159d72a821 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -85,7 +85,7 @@ Cura.MachineAction { id: editButton text: catalog.i18nc("@action:button", "Edit") - enabled: base.selectedPrinter != null && (base.selectedPrinter.getKey().substr(0,7) =="manual:") + enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true" onClicked: { manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress); @@ -96,7 +96,7 @@ Cura.MachineAction { id: removeButton text: catalog.i18nc("@action:button", "Remove") - enabled: base.selectedPrinter != null && (base.selectedPrinter.getKey().substr(0,7) =="manual:") + enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true" onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress) } @@ -155,7 +155,7 @@ Cura.MachineAction { base.selectedPrinter = listview.model[currentIndex]; // Only allow connecting if the printer has responded to API query since the last refresh - base.completeProperties = base.selectedPrinter != null && (base.selectedPrinter.firmwareVersion != ""); + base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true"; } Component.onCompleted: manager.startDiscovery() delegate: Rectangle diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e2fe36f118..e3eb15f859 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -36,11 +36,12 @@ class AuthState(IntEnum): ## Network connected (wifi / lan) printer that uses the Ultimaker API @signalemitter class NetworkPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, key, address, properties): + def __init__(self, key, address, properties, api_prefix): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf + self._api_prefix = api_prefix self._gcode = None self._print_finished = True # _print_finsihed == False means we're halfway in a print @@ -94,8 +95,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._material_ids = [""] * self._num_extruders self._hotend_ids = [""] * self._num_extruders - self._api_version = "1" - self._api_prefix = "/api/v" + self._api_version + "/" self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print over network")) @@ -187,6 +186,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def getProperties(self): return self._properties + @pyqtSlot(str, result = str) + def getProperty(self, key): + key = key.encode("utf-8") + if key in self._properties: + return self._properties.get(key, b"").decode("utf-8") + else: + return "" + ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result = str) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 4843ab8975..8dc3cd8646 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -73,7 +73,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): name = address instance_name = "manual:%s" % address - properties = { b"name": name.encode("UTF-8"), b"incomplete": True } + properties = { b"name": name.encode("utf-8"), b"manual": b"true", b"incomplete": b"true" } if instance_name not in self._printers: # Add a preliminary printer instance @@ -111,7 +111,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): name = ("%s (%s)" % (system_info["name"], address)) instance_name = "manual:%s" % address - properties = { b"name": name.encode("UTF-8"), b"firmware_version": system_info["firmware"].encode("UTF-8") } + properties = { b"name": name.encode("utf-8"), b"firmware_version": system_info["firmware"].encode("utf-8"), b"manual": b"true" } if instance_name in self._printers: # Only replace the printer if it is still in the list of (manual) printers self.removePrinter(instance_name) @@ -139,7 +139,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addPrinter(self, name, address, properties): - printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) + printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix) self._printers[printer.getKey()] = printer global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"): From 4f9eeb6be649dc9c1a7a7f01602d4112464e46de Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Sat, 1 Oct 2016 13:06:38 +0200 Subject: [PATCH 269/438] Hide printer details until printer has divulged them CURA-2483 --- DiscoverUM3Action.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 159d72a821..bb00588cb3 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -215,6 +215,7 @@ Cura.MachineAction } Grid { + visible: base.completeProperties width: parent.width columns: 2 Label From 8e5dbb78a4f00a66294798de811a48e147aa55ea Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Sat, 1 Oct 2016 13:07:55 +0200 Subject: [PATCH 270/438] Don't connect to a printer if we don't have its properties yet CURA-2483 --- DiscoverUM3Action.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index bb00588cb3..a6ba96ab2a 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -30,7 +30,7 @@ Cura.MachineAction function connectToPrinter() { - if(base.selectedPrinter) + if(base.selectedPrinter && base.completeProperties) { var printerKey = base.selectedPrinter.getKey() if(connectingToPrinter != printerKey) { From e6570884fc54a4e440c5b99f89f9b39eda586ab4 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Sat, 1 Oct 2016 13:16:14 +0200 Subject: [PATCH 271/438] Hide dialog when accepting/rejecting Under some circumstances, UM.Dialog would not hide the dialog, so we do it ourselves. CURA-2384 --- DiscoverUM3Action.qml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index a6ba96ab2a..d583842dcf 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -313,11 +313,19 @@ Cura.MachineAction rightButtons: [ Button { text: catalog.i18nc("@action:button","Cancel") - onClicked: manualPrinterDialog.reject() + onClicked: + { + manualPrinterDialog.reject() + manualPrinterDialog.hide() + } }, Button { text: catalog.i18nc("@action:button", "Ok") - onClicked: manualPrinterDialog.accept() + onClicked: + { + manualPrinterDialog.accept() + manualPrinterDialog.hide() + } enabled: manualPrinterDialog.addressText.trim() != "" isDefault: true } From 8a6d1254786cfe7647195b809dc2071f08853697 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 3 Oct 2016 12:29:50 +0200 Subject: [PATCH 272/438] Add strings to clarify the manual printer UI Also adds resuming/pausing states strings and a "Print Again" to use elsewhere. --- DiscoverUM3Action.qml | 39 ++++++++++++++++++++++++++++------- NetworkPrinterOutputDevice.py | 7 +++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index d583842dcf..0cc6031e7f 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -54,7 +54,7 @@ Cura.MachineAction { id: pageTitle width: parent.width - text: catalog.i18nc("@title", "Connect to Networked Printer") + text: catalog.i18nc("@title:window", "Connect to Networked Printer") wrapMode: Text.WordWrap font.pointSize: 18 } @@ -64,7 +64,7 @@ Cura.MachineAction id: pageDescription width: parent.width wrapMode: Text.WordWrap - text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your Ultimaker 3, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your Ultimaker 3 from the list below:") + text: catalog.i18nc("@label", "To print directly to your printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your printer, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your printer from the list below:") } Row @@ -103,7 +103,7 @@ Cura.MachineAction Button { id: rediscoverButton - text: catalog.i18nc("@title", "Refresh") + text: catalog.i18nc("@action:button", "Refresh") onClicked: manager.restartDiscovery() } } @@ -195,7 +195,7 @@ Cura.MachineAction wrapMode: Text.WordWrap //: Tips label //TODO: get actual link from webteam - text: catalog.i18nc("@label", "If your Ultimaker 3 is not listed, read the Ultimaker 3 network troubleshooting guide").arg("https://ultimaker.com/en/troubleshooting"); + text: catalog.i18nc("@label", "If your printer is not listed, read the network-printing troubleshooting guide").arg("https://ultimaker.com/en/troubleshooting"); onLinkActivated: Qt.openUrlExternally(link) } @@ -228,7 +228,7 @@ Cura.MachineAction { width: parent.width * 0.5 wrapMode: Text.WordWrap - text: catalog.i18nc("@label", "Ultimaker 3") + text: true ? catalog.i18nc("@label", "Ultimaker 3") : catalog.i18nc("@label", "Ultimaker 3 Extended") } Label { @@ -246,7 +246,7 @@ Cura.MachineAction { width: parent.width * 0.5 wrapMode: Text.WordWrap - text: catalog.i18nc("@label", "IP Address") + text: catalog.i18nc("@label", "Address") } Label { @@ -255,6 +255,13 @@ Cura.MachineAction text: base.selectedPrinter ? base.selectedPrinter.ipAddress : "" } } + Label + { + width: parent.width + wrapMode: Text.WordWrap + visible: base.selectedPrinter && !base.completeProperties + text: catalog.i18nc("@label", "The printer at this address has not yet responded." ) + } Button { @@ -266,13 +273,20 @@ Cura.MachineAction } } + Label + { + // TODO: move use this in an appropriate location + visible: false + text: catalog.i18nc("@label:", "Print Again") + } + UM.Dialog { id: manualPrinterDialog property string printerKey property alias addressText: addressField.text - title: catalog.i18nc("@label", "IP Address") + title: catalog.i18nc("@title:window", "Printer Address") minimumWidth: 400 * Screen.devicePixelRatio minimumHeight: 120 * Screen.devicePixelRatio @@ -298,8 +312,17 @@ Cura.MachineAction Column { anchors.fill: parent + spacing: UM.Theme.getSize("default_margin").height - TextField { + Label + { + text: catalog.i18nc("@alabel","Enter the IP address or hostname of your printer on the network.") + width: parent.width + wrapMode: Text.WordWrap + } + + TextField + { id: addressField width: parent.width maximumLength: 40 diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e3eb15f859..288d01f975 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -804,6 +804,13 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Print aborted. Please check the printer")) state = "error" + # NB/TODO: the following two states are intentionally added for future proofing the i18n strings + # but are currently non-functional + if state == "!pausing": + self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Pausing print...")) + if state == "!resuming": + self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Resuming print...")) + self._updateJobState(state) self.setTimeElapsed(json_data["time_elapsed"]) self.setTimeTotal(json_data["time_total"]) From a31a4a1e90dd4aaf0f445abc359b3ad4c2875a86 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 4 Oct 2016 13:49:01 +0200 Subject: [PATCH 273/438] Reinitialise Zeroconf network socket upon refresh/start Reinitialising Zeroconf entirely causes CPU usage to go through the proverbial roof. This is sort of a hack, since we're touching the _listen_socket variable inside Zeroconf, which it hasn't exposed. But it works to still be able to refresh Zeroconf after network switches, and to not have high CPU usage afterwards. Contributes to issue CURA-2497. --- NetworkPrinterOutputDevicePlugin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index bb1fade0bc..29f33a04de 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -1,7 +1,7 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from . import NetworkPrinterOutputDevice -from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo, new_socket from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.Application import Application @@ -20,7 +20,7 @@ import json class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def __init__(self): super().__init__() - self._zero_conf = None + self._zero_conf = Zeroconf() self._browser = None self._printers = {} @@ -53,16 +53,18 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.startDiscovery() def startDiscovery(self): - self.stop() if self._browser: self._browser.cancel() self._browser = None self._old_printers = [printer_name for printer_name in self._printers] self._printers = {} self.printerListChanged.emit() - # After network switching, one must make a new instance of Zeroconf - # On windows, the instance creation is very fast (unnoticable). Other platforms? - self._zero_conf = Zeroconf() + #After network switching, Zeroconf's network socket is no longer functional. + #Zeroconf must reinitialise its socket, but reinitialising Zeroconf causes massive CPU usage. + #So we only reinitialise Zeroconf's listening socket. + self._zero_conf.engine.del_reader(self._zero_conf._listen_socket) + self._zero_conf._listen_socket = new_socket() #Warning: Touching Zeroconf's privates! It has no functionality to reinitialise its own socket. + self._zero_conf.engine.add_reader(self._zero_conf.listener, self._zero_conf._listen_socket) self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) # Look for manual instances from preference @@ -123,8 +125,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): ## Stop looking for devices on network. def stop(self): - if self._zero_conf is not None: - self._zero_conf.close() + self._zero_conf.close() def getPrinters(self): return self._printers From 8bd3b7ea9b140340d4c69bff60f09101563a764e Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 4 Oct 2016 14:50:08 +0200 Subject: [PATCH 274/438] Revert "Reinitialise Zeroconf network socket upon refresh/start" This reverts commit a31a4a1e90dd4aaf0f445abc359b3ad4c2875a86. --- NetworkPrinterOutputDevicePlugin.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 29f33a04de..bb1fade0bc 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -1,7 +1,7 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from . import NetworkPrinterOutputDevice -from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo, new_socket +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.Application import Application @@ -20,7 +20,7 @@ import json class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): def __init__(self): super().__init__() - self._zero_conf = Zeroconf() + self._zero_conf = None self._browser = None self._printers = {} @@ -53,18 +53,16 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.startDiscovery() def startDiscovery(self): + self.stop() if self._browser: self._browser.cancel() self._browser = None self._old_printers = [printer_name for printer_name in self._printers] self._printers = {} self.printerListChanged.emit() - #After network switching, Zeroconf's network socket is no longer functional. - #Zeroconf must reinitialise its socket, but reinitialising Zeroconf causes massive CPU usage. - #So we only reinitialise Zeroconf's listening socket. - self._zero_conf.engine.del_reader(self._zero_conf._listen_socket) - self._zero_conf._listen_socket = new_socket() #Warning: Touching Zeroconf's privates! It has no functionality to reinitialise its own socket. - self._zero_conf.engine.add_reader(self._zero_conf.listener, self._zero_conf._listen_socket) + # After network switching, one must make a new instance of Zeroconf + # On windows, the instance creation is very fast (unnoticable). Other platforms? + self._zero_conf = Zeroconf() self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) # Look for manual instances from preference @@ -125,7 +123,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): ## Stop looking for devices on network. def stop(self): - self._zero_conf.close() + if self._zero_conf is not None: + self._zero_conf.close() def getPrinters(self): return self._printers From 029fda72e340850526a2dc58af56c49250fbb0e8 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 4 Oct 2016 19:09:10 +0200 Subject: [PATCH 275/438] Fix warning when there is no current printer --- DiscoverUM3Action.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 0cc6031e7f..79f52460de 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -259,7 +259,7 @@ Cura.MachineAction { width: parent.width wrapMode: Text.WordWrap - visible: base.selectedPrinter && !base.completeProperties + visible: base.selectedPrinter != null && !base.completeProperties text: catalog.i18nc("@label", "The printer at this address has not yet responded." ) } From 8cbd6443be6dee070a225d2e444fa3080204c0e2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Oct 2016 14:31:41 +0200 Subject: [PATCH 276/438] UM3 familiy printers now set their machine type CURA-2475 --- NetworkPrinterOutputDevice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 288d01f975..f57637af1f 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -166,6 +166,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._compressing_print = False + printer_type = self._properties.get(b"machine", b"").decode("utf-8") + if printer_type == "9511.0": + self._updatePrinterType("UM3Extended") + elif printer_type == "9066.0": + self._updatePrinterType("UM3") + else: + self._updatePrinterType("unknown") + def _onNetworkAccesibleChanged(self, accessible): Logger.log("d", "Network accessible state changed to: %s", accessible) From ec63d6931edbebdab5f3d9a5a435f46dee6c13f5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Oct 2016 14:54:22 +0200 Subject: [PATCH 277/438] Actually perform the filtering based on machine type CURA-2475 --- DiscoverUM3Action.py | 3 +++ NetworkPrinterOutputDevice.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 3d69a8c118..8213812391 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -75,7 +75,10 @@ class DiscoverUM3Action(MachineAction): @pyqtProperty("QVariantList", notify = printersChanged) def foundDevices(self): if self._network_plugin: + global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId() printers = list(self._network_plugin.getPrinters().values()) + # TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet. + printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"] printers.sort(key = lambda k: k.name) return printers else: diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f57637af1f..3d56a777aa 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -168,9 +168,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): printer_type = self._properties.get(b"machine", b"").decode("utf-8") if printer_type == "9511.0": - self._updatePrinterType("UM3Extended") + self._updatePrinterType("ultimaker3_extended") elif printer_type == "9066.0": - self._updatePrinterType("UM3") + self._updatePrinterType("ultimaker3") else: self._updatePrinterType("unknown") From f71b23c72aadf3b179e81d5fc691da164b8af1e7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Oct 2016 09:35:31 +0200 Subject: [PATCH 278/438] Added handling for if there is no global stack CURA-2475 --- DiscoverUM3Action.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 8213812391..7d7484f7af 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -75,7 +75,11 @@ class DiscoverUM3Action(MachineAction): @pyqtProperty("QVariantList", notify = printersChanged) def foundDevices(self): if self._network_plugin: - global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId() + if Application.getInstance().getGlobalContainerStack(): + global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId() + else: + global_printer_type = "unknown" + printers = list(self._network_plugin.getPrinters().values()) # TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet. printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"] From 2688928467d602d808cbe2d5efaddae9bd98a946 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Oct 2016 11:30:56 +0200 Subject: [PATCH 279/438] Revision number is no longer taken into account CURA-2475 --- DiscoverUM3Action.py | 2 +- NetworkPrinterOutputDevice.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 7d7484f7af..c192d36ac1 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -79,7 +79,7 @@ class DiscoverUM3Action(MachineAction): global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId() else: global_printer_type = "unknown" - + printers = list(self._network_plugin.getPrinters().values()) # TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet. printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"] diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 3d56a777aa..2eb126d966 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -167,9 +167,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._compressing_print = False printer_type = self._properties.get(b"machine", b"").decode("utf-8") - if printer_type == "9511.0": + if printer_type.startswith("9511"): self._updatePrinterType("ultimaker3_extended") - elif printer_type == "9066.0": + elif printer_type.startswith("9066"): self._updatePrinterType("ultimaker3") else: self._updatePrinterType("unknown") From 93891490647952219ba75e6a698137b753dcd7a8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Oct 2016 11:36:26 +0200 Subject: [PATCH 280/438] Type name display is now also updated in print discovery CURA-2475 --- DiscoverUM3Action.qml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 79f52460de..06b701605e 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -228,7 +228,26 @@ Cura.MachineAction { width: parent.width * 0.5 wrapMode: Text.WordWrap - text: true ? catalog.i18nc("@label", "Ultimaker 3") : catalog.i18nc("@label", "Ultimaker 3 Extended") + text: + { + if(base.selectedPrinter) + { + if(base.selectedPrinter.printerType == "ultimaker3") + { + return catalog.i18nc("@label", "Ultimaker 3") + } else if(base.selectedPrinter.printerType == "ultimaker3_extended") + { + return catalog.i18nc("@label", "Ultimaker 3 Extended") + } else + { + return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field' + } + } + else + { + return "" + } + } } Label { From afd5df283c5f09dc821dfde4bdc9c0549f76e95f Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 6 Oct 2016 14:33:42 +0200 Subject: [PATCH 281/438] Re-filter list of printers when you open the window When you open the window of a printer that already exists, it doesn't re-create the entire window and therefore didn't re-apply the filter on machine type. This triggers the filter to be applied again. Contributes to issue CURA-2475. --- DiscoverUM3Action.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index c192d36ac1..c4ffdb8472 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -39,6 +39,11 @@ class DiscoverUM3Action(MachineAction): self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() + ## Re-filters the list of printers. + @pyqtSlot() + def reset(self): + self.printersChanged.emit() + @pyqtSlot() def restartDiscovery(self): # Ensure that there is a bit of time after a printer has been discovered. From 26383658390012425c1982cf88b2a573fe359f2c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 12 Oct 2016 14:36:41 +0200 Subject: [PATCH 282/438] LastRequestTime is reset on connection close CURA-2630 --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2eb126d966..a1bb52c514 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -464,6 +464,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Reset timeout state self._connection_state_before_timeout = None self._last_response_time = time() + self._last_request_time = None # Stop update timers self._update_timer.stop() From a9b45572cc0343ee3f83353bf92d3cacc7dfcfb7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 12 Oct 2016 14:50:58 +0200 Subject: [PATCH 283/438] PostReply is now always reset correctly CURA-2630 --- NetworkPrinterOutputDevice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index a1bb52c514..2fd433775f 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -631,6 +631,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._compressing_print = False if self._post_reply: self._post_reply.abort() + self._post_reply = None Application.getInstance().showPrintMonitor.emit(False) ## Attempt to start a new print. @@ -747,6 +748,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._post_reply.abort() self._post_reply.uploadProgress.disconnect(self._onUploadProgress) Logger.log("d", "Uploading of print failed after %s", time() - self._send_gcode_start) + self._post_reply = None self._progress_message.hide() self.setConnectionState(ConnectionState.error) @@ -901,6 +903,9 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): elif "print_job" in reply_url: reply.uploadProgress.disconnect(self._onUploadProgress) Logger.log("d", "Uploading of print succeeded after %s", time() - self._send_gcode_start) + # Only reset the _post_reply if it was the same one. + if reply == self._post_reply: + self._post_reply = None self._progress_message.hide() elif reply.operation() == QNetworkAccessManager.PutOperation: From 2337a78a71e36386a687f8e8a7f0be0703ea45ba Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 12 Oct 2016 15:08:23 +0200 Subject: [PATCH 284/438] Camera requests now also set last_request_time CURA-2630 --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2fd433775f..934092f33c 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -230,6 +230,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): url = QUrl("http://" + self._address + ":8080/?action=snapshot") image_request = QNetworkRequest(url) self._manager.get(image_request) + self._last_request_time = time() ## Set the authentication state. # \param auth_state \type{AuthState} Enum value representing the new auth state From 91521eb49d1e6c0664fdc98b944d0e09e961535b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 12 Oct 2016 16:02:35 +0200 Subject: [PATCH 285/438] Added logging if connection to network printer was closed CURA-2630 --- NetworkPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 934092f33c..2581a4c3ca 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -440,6 +440,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def close(self): + Logger.log("d", "Closing connection of printer %s with ip %s", self._key, self._address) self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: From dc34b898d46da7af94ac2ad368fac7d9c0c70249 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 12 Oct 2016 16:35:22 +0200 Subject: [PATCH 286/438] Added max recreateNetworkManager count increase per update CURA-2630 --- NetworkPrinterOutputDevice.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 2581a4c3ca..ac3af93c84 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -314,9 +314,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 + counter = 0 # Counter to prevent possible indefinite while loop. # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. - while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: + while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time and counter < 10: + counter += 1 self._recreate_network_manager_count += 1 Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() From 8951efd140fefd7af5d5e98d381558af5a291f7a Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 13 Oct 2016 11:26:10 +0200 Subject: [PATCH 287/438] Improve log message Instead of always reporting that it waits 30s, it reports the actual time it waits. Contributes to issue CURA-2630. --- NetworkPrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index ac3af93c84..619a70a952 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -320,7 +320,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time and counter < 10: counter += 1 self._recreate_network_manager_count += 1 - Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) + Logger.log("d", "Timeout lasted over %.0f seconds (%.1fs), re-checking connection.", self._recreate_network_manager_time, time_since_last_response) self._createNetworkManager() return @@ -495,7 +495,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): print_information = Application.getInstance().getPrintInformation() - # Check if PrintCores / materials are loaded at all. Any failure in these results in an Error. + # Check if print cores / materials are loaded at all. Any failure in these results in an error. for index in range(0, self._num_extruders): if print_information.materialLengths[index] != 0: if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "": From 4cc6fb08780f8471938e9b92469ddf80c6cbb70b Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 13 Oct 2016 18:53:22 +0200 Subject: [PATCH 288/438] Remove unused label Cleanup before release --- DiscoverUM3Action.qml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 79f52460de..219ec25f25 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -273,13 +273,6 @@ Cura.MachineAction } } - Label - { - // TODO: move use this in an appropriate location - visible: false - text: catalog.i18nc("@label:", "Print Again") - } - UM.Dialog { id: manualPrinterDialog From be356bae3744d0787adf02dd35552ad8b91863b4 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 17 Oct 2016 15:52:55 +0200 Subject: [PATCH 289/438] Allow for writing multiple meshes at the same time This is an API break in Uranium. It has no effect on this plug-in since this plug-in always writes the entire g-code anyway. Contributes to issue CURA-2617. --- NetworkPrinterOutputDevice.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 619a70a952..b59896123e 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -1,3 +1,6 @@ +# Copyright (c) 2016 Ultimaker B.V. +# Cura is released under the terms of the AGPLv3 or higher. + from UM.i18n import i18nCatalog from UM.Application import Application from UM.Logger import Logger @@ -474,7 +477,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.stop() self._camera_timer.stop() - def requestWrite(self, node, file_name = None, filter_by_machine = False): + ## Request the current scene to be sent to a network-connected printer. + # + # \param nodes A collection of scene nodes to send. This is ignored. + # \param file_name \type{string} A suggestion for a file name to write. + # This is ignored. + # \param filter_by_machine Whether to filter MIME types by machine. This + # is ignored. + def requestWrite(self, nodes, file_name = None, filter_by_machine = False): if self._progress != 0: self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to start a new print job because the printer is busy. Please check the printer.")) self._error_message.show() From 7427730123bff19d30c97795b8e7c2e533ea1613 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 18 Oct 2016 12:56:15 +0200 Subject: [PATCH 290/438] Update question to sync printer configuration UX designer wanted to have the text changed. Contributes to issue CURA-2634. --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index b59896123e..9419eeaf16 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -947,7 +947,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def materialHotendChangedMessage(self, callback): Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Changes on the Printer"), i18n_catalog.i18nc("@label", - "Do you want to change the PrintCores and materials in Cura to match your printer?"), + "Would you like to update your current printer configuration into Cura?"), i18n_catalog.i18nc("@label", "The PrintCores and/or materials on your printer were changed. For the best result, always slice for the PrintCores and materials that are inserted in your printer."), buttons=QMessageBox.Yes + QMessageBox.No, From b1f3280b2da711a61f4696465c12e8b47747a2e8 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 19 Oct 2016 13:03:23 +0200 Subject: [PATCH 291/438] No longer say this is a secret plug-in It is no longer secret since the UM3 is released. Contributes to issue CURA-2737. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e88ce593b..54e3ff5eae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # UM3NetworkPrintingPlugin -Secret plugin to enable wifi printing from Cura to UM3 +Plugin to enable wifi printing from Cura to UM3. Intructions ---- From 601bf20a962fbc5989daf50bfbc20124b0aef6d9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 26 Oct 2016 13:38:09 +0200 Subject: [PATCH 292/438] Changed the entry point of the material guid to non-depricated spot We depricated the GUID as entry point, but all printers support both right now. --- NetworkPrinterOutputDevice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 619a70a952..f674930dc8 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -68,12 +68,12 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # "extruders": [ # { # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, - # "active_material": {"GUID": "xxxxxxx", "length_remaining": -1.0}, + # "active_material": {"guid": "xxxxxxx", "length_remaining": -1.0}, # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "AA 0.4"} # }, # { # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, - # "active_material": {"GUID": "xxxx", "length_remaining": -1.0}, + # "active_material": {"guid": "xxxx", "length_remaining": -1.0}, # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "BB 0.4"} # } # ], @@ -421,7 +421,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): temperature = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["temperature"]["current"] self._setHotendTemperature(index, temperature) try: - material_id = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] + material_id = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] except KeyError: material_id = "" self._setMaterialId(index, material_id) @@ -504,7 +504,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No PrinterCore loaded in slot {0}".format(index + 1))) self._error_message.show() return - if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] == "": + if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "": Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1) self._error_message = Message( i18n_catalog.i18nc("@info:status", @@ -533,7 +533,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"}) if material: - remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["GUID"] + remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] if material.getMetaDataEntry("GUID") != remote_material_guid: Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, remote_material_guid, From 4900a8f38089f382c61ab838e9b0f9ba61732f26 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 26 Oct 2016 14:52:30 +0200 Subject: [PATCH 293/438] Added message to warn user if calibration was not properly done CURA-2731 --- NetworkPrinterOutputDevice.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f674930dc8..f61486e53b 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -545,9 +545,17 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): remote_material_name = remote_materials[0].getName() warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1)) + try: + is_offset_calibrated = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["offset"]["state"] == "valid" + except KeyError: # Older versions of the API don't expose the offset property, so we must asume that all is well. + is_offset_calibrated = True + + if not is_offset_calibrated: + warnings.append(i18n_catalog.i18nc("@label", "PrintCore {0} is not properly calibrated. XY calibration needs to be performed on the printer.").format(index + 1)) + if warnings: text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") - informative_text = i18n_catalog.i18nc("@label", "There is a mismatch between the configuration of the printer and Cura. " + informative_text = i18n_catalog.i18nc("@label", "There is a mismatch between the configuration or calibration of the printer and Cura. " "For the best result, always slice for the PrintCores and materials that are inserted in your printer.") detailed_text = "" for warning in warnings: From b41b98f058d650c923957553414971cf49e62e70 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 26 Oct 2016 16:50:04 +0200 Subject: [PATCH 294/438] OSX would get stuck and act like there was a modal dialog open if we do the callback processing immediately. Delay it slightly. CURA-2801 CLONE - Cura in freeze mode when printing via WiFi --- NetworkPrinterOutputDevice.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index f61486e53b..f70acc0513 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -574,10 +574,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.startPrint() def _configurationMismatchMessageCallback(self, button): - if button == QMessageBox.Yes: - self.startPrint() - else: - Application.getInstance().showPrintMonitor.emit(False) + def delayedCallback(): + if button == QMessageBox.Yes: + self.startPrint() + else: + Application.getInstance().showPrintMonitor.emit(False) + # For some unknown reason Cura on OSX will hang if we do the call back code + # immediately without first returning and leaving QML's event system. + QTimer.singleShot(100, delayedCallback) def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error From 688833d36ab23a83a743f69ecaf7cc2f03bfaeef Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 2 Nov 2016 09:31:51 +0100 Subject: [PATCH 295/438] Add better context for 'Print over network' text This should help the translators get it better. --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 1cf3fd7b9f..c06d6ada81 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -100,7 +100,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) - self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print over network")) + self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setIconName("print") From e7d04f7dc0e24e8060182b591f48d6a9a407a084 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 3 Nov 2016 17:32:17 +0100 Subject: [PATCH 296/438] Fixed some merge issues --- NetworkPrinterOutputDevice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index eef398da57..66ad0d44bf 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -131,7 +131,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._image_request = None self._image_reply = None - self._use_stream = True + self._use_stream = False self._stream_buffer = b"" self._stream_buffer_start_index = -1 @@ -491,11 +491,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.stop() self._camera_timer.stop() - if self._image_reply: + if self._image_reply: self._image_reply.abort() self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) self._image_reply = None - self._image_request = None + self._image_request = None ## Request the current scene to be sent to a network-connected printer. # From 09a64c7ba054e2e15475df63449731b5a1a76ba3 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Fri, 14 Oct 2016 15:30:45 +0200 Subject: [PATCH 297/438] JSON feat: anti support meshes (CURA-2077) --- resources/definitions/fdmprinter.def.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index e3b75d3813..381922fadd 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3693,6 +3693,17 @@ "settable_per_meshgroup": false, "settable_globally": false }, + "anti_support_mesh": + { + "label": "Anti Overhang Mesh", + "description": "Use this mesh to specify where no part of the model should be detected as overhang. This can be used to remove unwanted support structure.", + "type": "bool", + "default_value": false, + "settable_per_mesh": true, + "settable_per_extruder": false, + "settable_per_meshgroup": false, + "settable_globally": false + }, "magic_mesh_surface_mode": { "label": "Surface Mode", From 2c87cddd3c9c1688ee49b17f42dcd2c38627fd3d Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Fri, 14 Oct 2016 15:32:10 +0200 Subject: [PATCH 298/438] JSON refactor: anti_support_mesh ==> anti_overhang_mesh (CURA-2077) --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 381922fadd..d7ec656a29 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3693,7 +3693,7 @@ "settable_per_meshgroup": false, "settable_globally": false }, - "anti_support_mesh": + "anti_overhang_mesh": { "label": "Anti Overhang Mesh", "description": "Use this mesh to specify where no part of the model should be detected as overhang. This can be used to remove unwanted support structure.", From b84071196022155a9086654ae44dfff7bbb1872d Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Fri, 14 Oct 2016 15:48:55 +0200 Subject: [PATCH 299/438] JSON feat: support_mesh (CURA-2077) --- resources/definitions/fdmprinter.def.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index d7ec656a29..8cf709ee77 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3693,6 +3693,17 @@ "settable_per_meshgroup": false, "settable_globally": false }, + "support_mesh": + { + "label": "Support Mesh", + "description": "Use this mesh to specify support areas. This can be used to generate support structure.", + "type": "bool", + "default_value": false, + "settable_per_mesh": true, + "settable_per_extruder": false, + "settable_per_meshgroup": false, + "settable_globally": false + }, "anti_overhang_mesh": { "label": "Anti Overhang Mesh", From 1787b1e65be90396796f2800e95afef5e747ac09 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 19 Oct 2016 17:26:18 +0200 Subject: [PATCH 300/438] JSON refactor: support_mesh ==> overhang_mesh (CURA-2077) --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 8cf709ee77..0109f0344b 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3693,10 +3693,10 @@ "settable_per_meshgroup": false, "settable_globally": false }, - "support_mesh": + "overhang_mesh": { - "label": "Support Mesh", - "description": "Use this mesh to specify support areas. This can be used to generate support structure.", + "label": "Overhang Mesh", + "description": "Use this mesh to specify areas which should be supported. This can be used to generate support structure.", "type": "bool", "default_value": false, "settable_per_mesh": true, From 1b853de9810abb619a54e1086772c4146ec55cf8 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 2 Nov 2016 15:55:20 +0100 Subject: [PATCH 301/438] Revert "JSON refactor: support_mesh ==> overhang_mesh (CURA-2077)" This reverts commit d675ffe6e9765e5118b85585502aee7a1cb3283c. --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 0109f0344b..8cf709ee77 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3693,10 +3693,10 @@ "settable_per_meshgroup": false, "settable_globally": false }, - "overhang_mesh": + "support_mesh": { - "label": "Overhang Mesh", - "description": "Use this mesh to specify areas which should be supported. This can be used to generate support structure.", + "label": "Support Mesh", + "description": "Use this mesh to specify support areas. This can be used to generate support structure.", "type": "bool", "default_value": false, "settable_per_mesh": true, From bf670d325a8a0ed7a1106c99ec28e2404f22e6d1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 4 Nov 2016 09:42:05 +0100 Subject: [PATCH 302/438] Camera can now be started & stopped to prevent bandwith issues CURA-2411 --- NetworkPrinterOutputDevice.py | 42 +++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 66ad0d44bf..356f61b172 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -124,14 +124,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._update_timer.timeout.connect(self._update) self._camera_timer = QTimer() - self._camera_timer.setInterval(2000) # Todo: Add preference for camera update interval + self._camera_timer.setInterval(500) # Todo: Add preference for camera update interval self._camera_timer.setSingleShot(False) - self._camera_timer.timeout.connect(self._update_camera) + self._camera_timer.timeout.connect(self._updateCamera) self._image_request = None self._image_reply = None - self._use_stream = False + self._use_stream = True self._stream_buffer = b"" self._stream_buffer_start_index = -1 @@ -233,14 +233,28 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def ipAddress(self): return self._address - def _start_camera_stream(self): + def _stopCamera(self): + self._camera_timer.stop() + if self._image_reply: + self._image_reply.abort() + self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) + self._image_reply = None + self._image_request = None + + def _startCamera(self): + if self._use_stream: + self._startCameraStream() + else: + self._camera_timer.start() + + def _startCameraStream(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=stream") self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) - def _update_camera(self): + def _updateCamera(self): if not self._manager.networkAccessible(): return ## Request new image @@ -489,13 +503,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # Stop update timers self._update_timer.stop() - self._camera_timer.stop() - - if self._image_reply: - self._image_reply.abort() - self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) - self._image_reply = None - self._image_request = None + + self.stopCamera() ## Request the current scene to be sent to a network-connected printer. # @@ -625,7 +634,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. if not self._use_stream: - self._update_camera() + self._updateCamera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) ## Check if this machine was authenticated before. @@ -633,10 +642,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._authentication_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_key", None) self._update_timer.start() - if self._use_stream: - self._start_camera_stream() - else: - self._camera_timer.start() + #self.startCamera() ## Stop requesting data from printer def disconnect(self): @@ -969,6 +975,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def _onStreamDownloadProgress(self, bytes_received, bytes_total): # An MJPG stream is (for our purpose) a stream of concatenated JPG images. # JPG images start with the marker 0xFFD8, and end with 0xFFD9 + if self._image_reply is None: + return self._stream_buffer += self._image_reply.readAll() if self._stream_buffer_start_index == -1: From efc9719ff08708af98d9275b77f198bf6a9a2fb0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 10:00:00 +0100 Subject: [PATCH 303/438] Added stub workspace reader CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 12 ++++++++++++ plugins/3MFReader/__init__.py | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 plugins/3MFReader/ThreeMFWorkspaceReader.py diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py new file mode 100644 index 0000000000..c1c78037dd --- /dev/null +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -0,0 +1,12 @@ +from UM.Workspace.WorkspaceReader import WorkspaceReader + + +## Base implementation for reading 3MF workspace files. +class ThreeMFWorkspaceReader(WorkspaceReader): + def __init__(self): + super().__init__() + + def preRead(self, file_name): + return WorkspaceReader.PreReadResult.accepted + # TODO: Find 3MFFileReader so we can load SceneNodes + # TODO: Ask user if it's okay for the scene to be cleared diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index 42b1794160..a2af30211d 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -2,10 +2,11 @@ # Cura is released under the terms of the AGPLv3 or higher. from . import ThreeMFReader - +from . import ThreeMFWorkspaceReader from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") + def getMetaData(): return { "plugin": { @@ -20,8 +21,17 @@ def getMetaData(): "extension": "3mf", "description": catalog.i18nc("@item:inlistbox", "3MF File") } + ], + "workspace_reader": + [ + { + "extension": "3mf", + "description": catalog.i18nc("@item:inlistbox", "3MF File") + } ] } + def register(app): - return { "mesh_reader": ThreeMFReader.ThreeMFReader() } + return {"mesh_reader": ThreeMFReader.ThreeMFReader(), + "workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()} From a859c9883c404cef61583b1d1dd383bad90a2116 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 15:33:31 +0100 Subject: [PATCH 304/438] Added loadWorkspace option to menu CURA-1263 --- resources/qml/Actions.qml | 7 +++++++ resources/qml/Cura.qml | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index e88b7e77ea..2719d09cbc 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -11,6 +11,7 @@ import Cura 1.0 as Cura Item { property alias open: openAction; + property alias loadWorkspace: loadWorkspaceAction; property alias quit: quitAction; property alias undo: undoAction; @@ -286,6 +287,12 @@ Item shortcut: StandardKey.Open; } + Action + { + id: loadWorkspaceAction + text: catalog.i18nc("@action:inmenu menubar:file","&Load Workspace..."); + } + Action { id: showEngineLogAction; diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 021178e6db..94e81190a6 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -67,9 +67,14 @@ UM.MainWindow id: fileMenu title: catalog.i18nc("@title:menu menubar:toplevel","&File"); - MenuItem { + MenuItem + { action: Cura.Actions.open; } + MenuItem + { + action: Cura.Actions.loadWorkspace + } RecentFilesMenu { } @@ -712,6 +717,38 @@ UM.MainWindow onTriggered: openDialog.open() } + FileDialog + { + id: openWorkspaceDialog; + + //: File open dialog title + title: catalog.i18nc("@title:window","Open workspace") + modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal; + selectMultiple: false + nameFilters: UM.WorkspaceFileHandler.supportedReadFileTypes; + folder: CuraApplication.getDefaultPath("dialog_load_path") + onAccepted: + { + //Because several implementations of the file dialog only update the folder + //when it is explicitly set. + var f = folder; + folder = f; + + CuraApplication.setDefaultPath("dialog_load_path", folder); + + for(var i in fileUrls) + { + UM.WorkspaceFileHandler.readLocalFile(fileUrls[i]) + } + } + } + + Connections + { + target: Cura.Actions.loadWorkspace + onTriggered:openWorkspaceDialog.open() + } + EngineLog { id: engineLog; From 164f378dd4cdd1e335a64c37f746ded91d287bda Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 17:26:01 +0100 Subject: [PATCH 305/438] Added supported Extensions to workspace reader CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c1c78037dd..3250706b97 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -5,6 +5,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): super().__init__() + self._supported_extensions = [".3mf"] def preRead(self, file_name): return WorkspaceReader.PreReadResult.accepted From e30038435cf24395ac38c730b61330006f456c7e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 17:36:41 +0100 Subject: [PATCH 306/438] Added pre-read check for 3mf Reader CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 3250706b97..c46b83bd1f 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -1,5 +1,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader +from UM.Application import Application +from UM.Logger import Logger ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): @@ -7,7 +9,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): super().__init__() self._supported_extensions = [".3mf"] + self._3mf_mesh_reader = None + def preRead(self, file_name): - return WorkspaceReader.PreReadResult.accepted - # TODO: Find 3MFFileReader so we can load SceneNodes + self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) + if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted: + pass + else: + Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace") + return WorkspaceReader.PreReadResult.failed # TODO: Ask user if it's okay for the scene to be cleared + return WorkspaceReader.PreReadResult.accepted + + def read(self, file_name): + pass From 304696c8090a10b14b3f7e8160b5569d37432b9c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 9 Nov 2016 14:03:57 +0100 Subject: [PATCH 307/438] OutputDevices now take file_handler into account CURA-1263 --- cura/PrinterOutputDevice.py | 2 +- .../RemovableDriveOutputDevice.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index f90566c30b..efabeae641 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -49,7 +49,7 @@ class PrinterOutputDevice(QObject, OutputDevice): self._printer_state = "" self._printer_type = "unknown" - def requestWrite(self, nodes, file_name = None, filter_by_machine = False): + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): raise NotImplementedError("requestWrite needs to be implemented") ## Signals diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index 3fdd6b3e3e..b6505e7e6b 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -6,7 +6,7 @@ import os.path from UM.Application import Application from UM.Logger import Logger from UM.Message import Message -from UM.Mesh.WriteMeshJob import WriteMeshJob +from UM.FileHandler.WriteFileJob import WriteFileJob from UM.Mesh.MeshWriter import MeshWriter from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.OutputDevice.OutputDevice import OutputDevice @@ -37,13 +37,17 @@ class RemovableDriveOutputDevice(OutputDevice): # meshes. # \param limit_mimetypes Should we limit the available MIME types to the # MIME types available to the currently active machine? - def requestWrite(self, nodes, file_name = None, filter_by_machine = False): + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() # Formats supported by this application (File types that we can actually write) - file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() + if file_handler: + file_formats = file_handler.getSupportedFileTypesWrite() + else: + file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() + if filter_by_machine: container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"}) @@ -58,7 +62,11 @@ class RemovableDriveOutputDevice(OutputDevice): raise OutputDeviceError.WriteRequestFailedError() # Just take the first file format available. - writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) + if file_handler is not None: + writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"]) + else: + writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) + extension = file_formats[0]["extension"] if file_name is None: @@ -72,7 +80,7 @@ class RemovableDriveOutputDevice(OutputDevice): Logger.log("d", "Writing to %s", file_name) # Using buffering greatly reduces the write time for many lines of gcode self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") - job = WriteMeshJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode) + job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) From f57a17577f6f950140c927febd0ddf42d1ab78bf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 9 Nov 2016 14:14:22 +0100 Subject: [PATCH 308/438] Added workspace save option to menu CURA-1263 --- resources/qml/Cura.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 94e81190a6..0c0eff2f66 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -107,6 +107,12 @@ UM.MainWindow onObjectRemoved: saveAllMenu.removeItem(object) } } + MenuItem + { + id: saveWorkspaceMenu + text: catalog.i18nc("@title:menu menubar:file","Save Workspace") + onTriggered: UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, { "filter_by_machine": false, "file_type": "workspace" }); + } MenuItem { action: Cura.Actions.reloadAll; } From 54040d4c992aa2b1630b48ce111db93243fd0015 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 10:39:20 +0100 Subject: [PATCH 309/438] Moved 3mf writer here from Uranium CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 49 +++++ plugins/3MFWriter/ThreeMFWriter.py | 205 ++++++++++++++++++++ plugins/3MFWriter/__init__.py | 38 ++++ plugins/__init__.py | 0 4 files changed, 292 insertions(+) create mode 100644 plugins/3MFWriter/ThreeMFWorkspaceWriter.py create mode 100644 plugins/3MFWriter/ThreeMFWriter.py create mode 100644 plugins/3MFWriter/__init__.py create mode 100644 plugins/__init__.py diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py new file mode 100644 index 0000000000..cfc3e18eb1 --- /dev/null +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -0,0 +1,49 @@ +from UM.Workspace.WorkspaceWriter import WorkspaceWriter +from UM.Application import Application +from UM.Preferences import Preferences +import zipfile +from io import StringIO + + +class ThreeMFWorkspaceWriter(WorkspaceWriter): + def __init__(self): + super().__init__() + + def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): + mesh_writer = Application.getInstance().getMeshFileHandler().getWriter("3MFWriter") + + if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace + return False + + # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it). + mesh_writer.setStoreArchive(True) + mesh_writer.write(stream, nodes, mode) + archive = mesh_writer.getArchive() + + # Add global container stack data to the archive. + global_container_stack = Application.getInstance().getGlobalContainerStack() + global_stack_file = zipfile.ZipInfo("Cura/%s.stack.cfg" % global_container_stack.getId()) + global_stack_file.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(global_stack_file, global_container_stack.serialize()) + + # Write user changes to the archive. + global_user_instance_container = global_container_stack.getTop() + global_user_instance_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_user_instance_container.getId()) + global_user_instance_container.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(global_user_instance_file, global_user_instance_container.serialize()) + + # Write quality changes to the archive. + global_quality_changes = global_container_stack.findContainer({"type": "quality_changes"}) + global_quality_changes_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_quality_changes.getId()) + global_quality_changes.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(global_quality_changes_file, global_quality_changes.serialize()) + + # Write preferences to archive + preferences_file = zipfile.ZipInfo("Cura/preferences.cfg") + preferences_string = StringIO() + Preferences.getInstance().writeToFile(preferences_string) + archive.writestr(preferences_file, preferences_string.getvalue()) + # Close the archive & reset states. + archive.close() + mesh_writer.setStoreArchive(False) + return True diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py new file mode 100644 index 0000000000..acf1421655 --- /dev/null +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -0,0 +1,205 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Uranium is released under the terms of the AGPLv3 or higher. + +from UM.Mesh.MeshWriter import MeshWriter +from UM.Math.Vector import Vector +from UM.Logger import Logger +from UM.Math.Matrix import Matrix +from UM.Settings.SettingRelation import RelationType + +try: + import xml.etree.cElementTree as ET +except ImportError: + Logger.log("w", "Unable to load cElementTree, switching to slower version") + import xml.etree.ElementTree as ET + +import zipfile +import UM.Application + + +class ThreeMFWriter(MeshWriter): + def __init__(self): + super().__init__() + self._namespaces = { + "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", + "content-types": "http://schemas.openxmlformats.org/package/2006/content-types", + "relationships": "http://schemas.openxmlformats.org/package/2006/relationships", + "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10" + } + + self._unit_matrix_string = self._convertMatrixToString(Matrix()) + self._archive = None + self._store_archive = False + + def _convertMatrixToString(self, matrix): + result = "" + result += str(matrix._data[0,0]) + " " + result += str(matrix._data[1,0]) + " " + result += str(matrix._data[2,0]) + " " + result += str(matrix._data[0,1]) + " " + result += str(matrix._data[1,1]) + " " + result += str(matrix._data[2,1]) + " " + result += str(matrix._data[0,2]) + " " + result += str(matrix._data[1,2]) + " " + result += str(matrix._data[2,2]) + " " + result += str(matrix._data[0,3]) + " " + result += str(matrix._data[1,3]) + " " + result += str(matrix._data[2,3]) + " " + return result + + ## Should we store the archive + # Note that if this is true, the archive will not be closed. + # The object that set this parameter is then responsible for closing it correctly! + def setStoreArchive(self, store_archive): + self._store_archive = store_archive + + def getArchive(self): + return self._archive + + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): + try: + MeshWriter._meshNodes(nodes).__next__() + except StopIteration: + return False #Don't write anything if there is no mesh data. + self._archive = None # Reset archive + archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) + try: + model_file = zipfile.ZipInfo("3D/3dmodel.model") + # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. + model_file.compress_type = zipfile.ZIP_DEFLATED + + # Create content types file + content_types_file = zipfile.ZipInfo("[Content_Types].xml") + content_types_file.compress_type = zipfile.ZIP_DEFLATED + content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) + rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") + model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") + + # Create _rels/.rels file + relations_file = zipfile.ZipInfo("_rels/.rels") + relations_file.compress_type = zipfile.ZIP_DEFLATED + relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) + model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") + + model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"]) + resources = ET.SubElement(model, "resources") + build = ET.SubElement(model, "build") + + added_nodes = [] + + # Write all nodes with meshData to the file as objects inside the resource tag + for index, n in enumerate(MeshWriter._meshNodes(nodes)): + added_nodes.append(n) # Save the nodes that have mesh data + object = ET.SubElement(resources, "object", id = str(index+1), type = "model") + mesh = ET.SubElement(object, "mesh") + + mesh_data = n.getMeshData() + vertices = ET.SubElement(mesh, "vertices") + verts = mesh_data.getVertices() + + if verts is None: + Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.") + continue # No mesh data, nothing to do. + if mesh_data.hasIndices(): + for face in mesh_data.getIndices(): + v1 = verts[face[0]] + v2 = verts[face[1]] + v3 = verts[face[2]] + xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2])) + xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2])) + xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2])) + + triangles = ET.SubElement(mesh, "triangles") + for face in mesh_data.getIndices(): + triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2])) + else: + triangles = ET.SubElement(mesh, "triangles") + for idx, vert in enumerate(verts): + xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2])) + + # If we have no faces defined, assume that every three subsequent vertices form a face. + if idx % 3 == 0: + triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2)) + + # Handle per object settings + stack = n.callDecoration("getStack") + if stack is not None: + changed_setting_keys = set(stack.getTop().getAllKeys()) + + # Ensure that we save the extruder used for this object. + if stack.getProperty("machine_extruder_count", "value") > 1: + changed_setting_keys.add("extruder_nr") + + settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"]) + + # Get values for all changed settings & save them. + for key in changed_setting_keys: + setting_xml = ET.SubElement(settings_xml, "setting", key = key) + setting_xml.text = str(stack.getProperty(key, "value")) + + # Add one to the index as we haven't incremented the last iteration. + index += 1 + nodes_to_add = set() + + for node in added_nodes: + # Check the parents of the nodes with mesh_data and ensure that they are also added. + parent_node = node.getParent() + while parent_node is not None: + if parent_node.callDecoration("isGroup"): + nodes_to_add.add(parent_node) + parent_node = parent_node.getParent() + else: + parent_node = None + + # Sort all the nodes by depth (so nodes with the highest depth are done first) + sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True) + + # We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene + for node in sorted_nodes_to_add: + object = ET.SubElement(resources, "object", id=str(index + 1), type="model") + components = ET.SubElement(object, "components") + for child in node.getChildren(): + if child in added_nodes: + component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation())) + index += 1 + added_nodes.append(node) + + # Create a transformation Matrix to convert from our worldspace into 3MF. + # First step: flip the y and z axis. + transformation_matrix = Matrix() + transformation_matrix._data[1, 1] = 0 + transformation_matrix._data[1, 2] = -1 + transformation_matrix._data[2, 1] = 1 + transformation_matrix._data[2, 2] = 0 + + global_container_stack = UM.Application.getInstance().getGlobalContainerStack() + # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the + # build volume. + if global_container_stack: + translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, + y=global_container_stack.getProperty("machine_depth", "value") / 2, + z=0) + translation_matrix = Matrix() + translation_matrix.setByTranslation(translation_vector) + transformation_matrix.preMultiply(translation_matrix) + + # Find out what the final build items are and add them. + for node in added_nodes: + if node.getParent().callDecoration("isGroup") is None: + node_matrix = node.getLocalTransformation() + + ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix))) + + archive.writestr(model_file, b' \n' + ET.tostring(model)) + archive.writestr(content_types_file, b' \n' + ET.tostring(content_types)) + archive.writestr(relations_file, b' \n' + ET.tostring(relations_element)) + except Exception as e: + Logger.logException("e", "Error writing zip file") + return False + finally: + if not self._store_archive: + archive.close() + else: + self._archive = archive + + return True diff --git a/plugins/3MFWriter/__init__.py b/plugins/3MFWriter/__init__.py new file mode 100644 index 0000000000..1dbc0bf281 --- /dev/null +++ b/plugins/3MFWriter/__init__.py @@ -0,0 +1,38 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Uranium is released under the terms of the AGPLv3 or higher. + +from UM.i18n import i18nCatalog +from . import ThreeMFWorkspaceWriter +from . import ThreeMFWriter + +i18n_catalog = i18nCatalog("uranium") + +def getMetaData(): + return { + "plugin": { + "name": i18n_catalog.i18nc("@label", "3MF Writer"), + "author": "Ultimaker", + "version": "1.0", + "description": i18n_catalog.i18nc("@info:whatsthis", "Provides support for writing 3MF files."), + "api": 3 + }, + "mesh_writer": { + "output": [{ + "extension": "3mf", + "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"), + "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode + }] + }, + "workspace_writer": { + "output": [{ + "extension": "3mf", + "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"), + "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode + }] + } + } + +def register(app): + return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()} diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 61d1199abfb97b0187eeee02d5f0ae52524f05ad Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:27:25 +0100 Subject: [PATCH 310/438] The entire machine is now saved to 3mf file when saving workspace CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 50 +++++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index cfc3e18eb1..1b00451e92 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -1,6 +1,8 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter from UM.Application import Application from UM.Preferences import Preferences +from UM.Settings.ContainerRegistry import ContainerRegistry +from cura.Settings.ExtruderManager import ExtruderManager import zipfile from io import StringIO @@ -20,30 +22,50 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): mesh_writer.write(stream, nodes, mode) archive = mesh_writer.getArchive() - # Add global container stack data to the archive. global_container_stack = Application.getInstance().getGlobalContainerStack() - global_stack_file = zipfile.ZipInfo("Cura/%s.stack.cfg" % global_container_stack.getId()) - global_stack_file.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(global_stack_file, global_container_stack.serialize()) - # Write user changes to the archive. - global_user_instance_container = global_container_stack.getTop() - global_user_instance_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_user_instance_container.getId()) - global_user_instance_container.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(global_user_instance_file, global_user_instance_container.serialize()) + # Add global container stack data to the archive. + self._writeContainerToArchive(global_container_stack, archive) - # Write quality changes to the archive. - global_quality_changes = global_container_stack.findContainer({"type": "quality_changes"}) - global_quality_changes_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_quality_changes.getId()) - global_quality_changes.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(global_quality_changes_file, global_quality_changes.serialize()) + # Also write all containers in the stack to the file + for container in global_container_stack.getContainers(): + self._writeContainerToArchive(container, archive) + + # Check if the machine has extruders and save all that data as well. + for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()): + self._writeContainerToArchive(extruder_stack, archive) + for container in extruder_stack.getContainers(): + self._writeContainerToArchive(container, archive) # Write preferences to archive preferences_file = zipfile.ZipInfo("Cura/preferences.cfg") preferences_string = StringIO() Preferences.getInstance().writeToFile(preferences_string) archive.writestr(preferences_file, preferences_string.getvalue()) + # Close the archive & reset states. archive.close() mesh_writer.setStoreArchive(False) return True + + @staticmethod + def _writeContainerToArchive(container, archive): + if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()): + return # Empty file, do nothing. + + file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).suffixes[0] + + # Some containers have a base file, which should then be the file to use. + base_file = container.getMetaDataEntry("base_file", None) + if base_file: + container = ContainerRegistry.getInstance().findContainers(id = base_file)[0] + + file_name = "Cura/%s.%s" % (container.getId(), file_suffix) + + if file_name in archive.namelist(): + return # File was already saved, no need to do it again. + + file_in_archive = zipfile.ZipInfo(file_name) + file_in_archive.compress_type = zipfile.ZIP_DEFLATED + + archive.writestr(file_in_archive, container.serialize()) From 3ab283bfed657280f954d46393ed785ecfc34d75 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:29:47 +0100 Subject: [PATCH 311/438] Saving workspace now works when there are no meshes to save CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 1b00451e92..437b9188ae 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -20,7 +20,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it). mesh_writer.setStoreArchive(True) mesh_writer.write(stream, nodes, mode) + archive = mesh_writer.getArchive() + if archive is None: # This happens if there was no mesh data to write. + archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) global_container_stack = Application.getInstance().getGlobalContainerStack() From d477630ccec0699a69f3b60d1a49610900c19001 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:34:12 +0100 Subject: [PATCH 312/438] Updated documentation CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 437b9188ae..afa3f53bf0 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -51,6 +51,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): mesh_writer.setStoreArchive(False) return True + ## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. + # \param container That follows the \type{ContainerInterface} to archive. + # \param archive The archive to write to. @staticmethod def _writeContainerToArchive(container, archive): if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()): @@ -66,9 +69,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): file_name = "Cura/%s.%s" % (container.getId(), file_suffix) if file_name in archive.namelist(): - return # File was already saved, no need to do it again. + return # File was already saved, no need to do it again. Uranium guarantees unique ID's, so this should hold. file_in_archive = zipfile.ZipInfo(file_name) + # For some reason we have to set the compress type of each file as well (it doesn't keep the type of the entire archive) file_in_archive.compress_type = zipfile.ZIP_DEFLATED archive.writestr(file_in_archive, container.serialize()) From 1f21957cb486e9dcaf0ba1abddf88ff7b4965dc3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:46:13 +0100 Subject: [PATCH 313/438] Fixed issue with per-object settings in 3mf reader CURA-1263 and CURA-382 --- plugins/3MFReader/ThreeMFReader.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 2aa719018d..3fee1adcb8 100644 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -84,20 +84,20 @@ class ThreeMFReader(MeshReader): definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom()) node.callDecoration("getStack").getTop().setDefinition(definition) - setting_container = node.callDecoration("getStack").getTop() - for setting in xml_settings: - setting_key = setting.get("key") - setting_value = setting.text + setting_container = node.callDecoration("getStack").getTop() + for setting in xml_settings: + setting_key = setting.get("key") + setting_value = setting.text - # Extruder_nr is a special case. - if setting_key == "extruder_nr": - extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value)) - if extruder_stack: - node.callDecoration("setActiveExtruder", extruder_stack.getId()) - else: - Logger.log("w", "Unable to find extruder in position %s", setting_value) - continue - setting_container.setProperty(setting_key,"value", setting_value) + # Extruder_nr is a special case. + if setting_key == "extruder_nr": + extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value)) + if extruder_stack: + node.callDecoration("setActiveExtruder", extruder_stack.getId()) + else: + Logger.log("w", "Unable to find extruder in position %s", setting_value) + continue + setting_container.setProperty(setting_key,"value", setting_value) if len(node.getChildren()) > 0: group_decorator = GroupDecorator() From 611572c324e3a1a065e49b5e71692e9987a241e6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 14:11:33 +0100 Subject: [PATCH 314/438] Extruder stack is now saved (instead of the material being saved as the stack) CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index afa3f53bf0..b6c9d884af 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -62,8 +62,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).suffixes[0] # Some containers have a base file, which should then be the file to use. - base_file = container.getMetaDataEntry("base_file", None) - if base_file: + if "base_file" in container.getMetaData(): + base_file = container.getMetaDataEntry("base_file") container = ContainerRegistry.getInstance().findContainers(id = base_file)[0] file_name = "Cura/%s.%s" % (container.getId(), file_suffix) From b92ca508bbe521f1fb1383361c3bbb6f140e6c97 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 15:42:20 +0100 Subject: [PATCH 315/438] Stacks are now loaded from workspace file CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 36 ++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c46b83bd1f..9046c5e6f3 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -2,6 +2,9 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.Application import Application from UM.Logger import Logger +from UM.Settings.ContainerStack import ContainerStack + +import zipfile ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): @@ -22,4 +25,35 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.accepted def read(self, file_name): - pass + # Load all the nodes / meshdata of the workspace + nodes = self._3mf_mesh_reader.read(file_name) + if nodes is None: + nodes = [] + + + archive = zipfile.ZipFile(file_name, "r") + + cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + + # Get the stack(s) saved in the workspace. + container_stack_files = [name for name in cura_file_names if name.endswith(".stack.cfg")] + global_stack = None + extruder_stacks = [] + for container_stack_file in container_stack_files: + container_id = container_stack_file.replace("Cura/", "") + container_id = container_id.replace(".stack.cfg", "") + stack = ContainerStack(container_id) + + # Serialize stack by converting read data from bytes to string + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + + if stack.getMetaDataEntry("type") == "extruder_train": + extruder_stacks.append(stack) + else: + global_stack = stack + + # Check if the right machine type is active now + #Application.getInstance().getGlobalContainerStack().getBottom().getId() == + + + return nodes From 4a2f07c3632a119e35100c6c3df5c0b62ad3f6a2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 10:39:54 +0100 Subject: [PATCH 316/438] Definitions & materials are now loaded from workspace CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 82 ++++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 9046c5e6f3..c18e823658 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -3,9 +3,13 @@ from UM.Application import Application from UM.Logger import Logger from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.DefinitionContainer import DefinitionContainer +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.ContainerRegistry import ContainerRegistry import zipfile + ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): @@ -30,30 +34,88 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if nodes is None: nodes = [] - + container_registry = ContainerRegistry.getInstance() archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few + # TODO: cases that the container loaded is the same (most notable in materials & definitions). + # TODO: It might be possible that we need to add smarter checking in the future. + + # Get all the definition files & check if they exist. If not, add them. + definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] + definition_container_files = [name for name in cura_file_names if name.endswith(definition_container_suffix)] + for definition_container_file in definition_container_files: + container_id = definition_container_file.replace("Cura/", "") + container_id = container_id.replace(".%s" % definition_container_suffix, "") + definitions = container_registry.findDefinitionContainers(id=container_id) + if not definitions: + definition_container = DefinitionContainer(container_id) + definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) + container_registry.addContainer(definition_container) + + # Get all the material files and check if they exist. If not, add them. + xml_material_profile = None + for type_name, container_type in container_registry.getContainerTypes(): + if type_name == "XmlMaterialProfile": + xml_material_profile = container_type + break + + if xml_material_profile: + material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] + material_container_files = [name for name in cura_file_names if name.endswith(material_container_suffix)] + for material_container_file in material_container_files: + container_id = material_container_file.replace("Cura/", "") + container_id = container_id.replace(".%s" % material_container_suffix, "") + materials = container_registry.findInstanceContainers(id=container_id) + if not materials: + material_container = xml_material_profile(container_id) + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) + container_registry.addContainer(material_container) + + # Get quality_changes and user profiles saved in the workspace + instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] + instance_container_files = [name for name in cura_file_names if name.endswith(instance_container_suffix)] + user_instance_containers = [] + quality_changes_instance_containers = [] + for instance_container_file in instance_container_files: + container_id = instance_container_file.replace("Cura/", "") + container_id = instance_container_file.replace(".%s" % instance_container_suffix, "") + instance_container = InstanceContainer(container_id) + + # Deserialize InstanceContainer by converting read data from bytes to string + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + container_type = instance_container.getMetaDataEntry("type") + if container_type == "user": + user_instance_containers.append(instance_container) + elif container_type == "quality_changes": + quality_changes_instance_containers.append(instance_container) + else: + continue + # Get the stack(s) saved in the workspace. - container_stack_files = [name for name in cura_file_names if name.endswith(".stack.cfg")] + '''container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] + container_stack_files = [name for name in cura_file_names if name.endswith(container_stack_suffix)] global_stack = None extruder_stacks = [] for container_stack_file in container_stack_files: container_id = container_stack_file.replace("Cura/", "") - container_id = container_id.replace(".stack.cfg", "") - stack = ContainerStack(container_id) + container_id = container_id.replace(".%s" % container_stack_suffix, "") - # Serialize stack by converting read data from bytes to string + # Check if a stack by this ID already exists; + container_stacks = container_registry.findContainerStacks(id = container_id) + if container_stacks: + print("CONTAINER ALREADY EXISTSSS") + + #stack = ContainerStack(container_id) + + # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: - global_stack = stack - - # Check if the right machine type is active now - #Application.getInstance().getGlobalContainerStack().getBottom().getId() == - + global_stack = stack''' return nodes From 413d788c0ca5c4161736464cbc347ff47bd6a4a0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 10:46:54 +0100 Subject: [PATCH 317/438] Fixed copypaste mistake CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c18e823658..500a5cdc7c 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -81,7 +81,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_instance_containers = [] for instance_container_file in instance_container_files: container_id = instance_container_file.replace("Cura/", "") - container_id = instance_container_file.replace(".%s" % instance_container_suffix, "") + container_id = container_id.replace(".%s" % instance_container_suffix, "") instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string From 99e753d3be061537e24cd8ea48bf881f3c49c39c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 13:31:36 +0100 Subject: [PATCH 318/438] Added loading of preferences form workspace CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 500a5cdc7c..6f31115c24 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -7,8 +7,10 @@ from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry -import zipfile +from UM.Preferences import Preferences +import zipfile +import io ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): @@ -39,6 +41,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + # Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its + # parsing code. + temp_preferences = Preferences() + temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks. + + # Copy a number of settings from the temp preferences to the global + global_preferences = Preferences.getInstance() + global_preferences.setValue("general/visible_settings", temp_preferences.getValue("general/visible_settings")) + global_preferences.setValue("cura/categories_expanded", temp_preferences.getValue("cura/categories_expanded")) + Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. @@ -90,10 +103,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if container_type == "user": user_instance_containers.append(instance_container) elif container_type == "quality_changes": + # Check if quality changes already exists. + quality_changes = container_registry.findInstanceContainers(id = container_id) + if not quality_changes: + container_registry.addContainer(instance_container) quality_changes_instance_containers.append(instance_container) else: continue + # Get the stack(s) saved in the workspace. '''container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] container_stack_files = [name for name in cura_file_names if name.endswith(container_stack_suffix)] From cbcc48ff3342f63d9f2e175e3227923adf095114 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 17:17:23 +0100 Subject: [PATCH 319/438] Pre-read now checks for conflicts and asks the user what strategy for resolvement to use CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 131 ++++++++++++++------ plugins/3MFReader/WorkspaceDialog.py | 79 ++++++++++++ plugins/3MFReader/WorkspaceDialog.qml | 52 ++++++++ 3 files changed, 221 insertions(+), 41 deletions(-) create mode 100644 plugins/3MFReader/WorkspaceDialog.py create mode 100644 plugins/3MFReader/WorkspaceDialog.qml diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 6f31115c24..e4a2c574ac 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -2,23 +2,34 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.Application import Application from UM.Logger import Logger +from UM.i18n import i18nCatalog from UM.Settings.ContainerStack import ContainerStack from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Preferences import Preferences - +from .WorkspaceDialog import WorkspaceDialog import zipfile import io +i18n_catalog = i18nCatalog("cura") + + ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): super().__init__() self._supported_extensions = [".3mf"] - + self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None + self._container_registry = ContainerRegistry.getInstance() + self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] + self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it + self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] + self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] + + self._resolvement_strategy = None def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -27,7 +38,45 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace") return WorkspaceReader.PreReadResult.failed - # TODO: Ask user if it's okay for the scene to be cleared + + # Check if there are any conflicts, so we can ask the user. + archive = zipfile.ZipFile(file_name, "r") + cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] + + conflict = False + for container_stack_file in container_stack_files: + container_id = self._stripFileToId(container_stack_file) + stacks = self._container_registry.findContainerStacks(id=container_id) + if stacks: + conflict = True + break + + # Check if any quality_changes instance container is in conflict. + if not conflict: + instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] + for instance_container_file in instance_container_files: + container_id = self._stripFileToId(instance_container_file) + instance_container = InstanceContainer(container_id) + + # Deserialize InstanceContainer by converting read data from bytes to string + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + container_type = instance_container.getMetaDataEntry("type") + if container_type == "quality_changes": + # Check if quality changes already exists. + quality_changes = self._container_registry.findInstanceContainers(id = container_id) + if quality_changes: + conflict = True + if conflict: + # There is a conflict; User should choose to either update the existing data, add everything as new data or abort + self._resolvement_strategy = None + self._dialog.show() + self._dialog.waitForClose() + if self._dialog.getResult() == "cancel": + return WorkspaceReader.PreReadResult.cancelled + + self._resolvement_strategy = self._dialog.getResult() + pass return WorkspaceReader.PreReadResult.accepted def read(self, file_name): @@ -36,7 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if nodes is None: nodes = [] - container_registry = ContainerRegistry.getInstance() archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] @@ -57,83 +105,84 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # TODO: It might be possible that we need to add smarter checking in the future. # Get all the definition files & check if they exist. If not, add them. - definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] - definition_container_files = [name for name in cura_file_names if name.endswith(definition_container_suffix)] + definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] for definition_container_file in definition_container_files: - container_id = definition_container_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % definition_container_suffix, "") - definitions = container_registry.findDefinitionContainers(id=container_id) + container_id = self._stripFileToId(definition_container_file) + definitions = self._container_registry.findDefinitionContainers(id=container_id) if not definitions: definition_container = DefinitionContainer(container_id) definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) - container_registry.addContainer(definition_container) + self._container_registry.addContainer(definition_container) # Get all the material files and check if they exist. If not, add them. - xml_material_profile = None - for type_name, container_type in container_registry.getContainerTypes(): - if type_name == "XmlMaterialProfile": - xml_material_profile = container_type - break - + xml_material_profile = self._getXmlProfileClass() + if self._material_container_suffix is None: + self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] if xml_material_profile: - material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] - material_container_files = [name for name in cura_file_names if name.endswith(material_container_suffix)] + material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)] for material_container_file in material_container_files: - container_id = material_container_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % material_container_suffix, "") - materials = container_registry.findInstanceContainers(id=container_id) + container_id = self._stripFileToId(material_container_file) + materials = self._container_registry.findInstanceContainers(id=container_id) if not materials: material_container = xml_material_profile(container_id) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) - container_registry.addContainer(material_container) + self._container_registry.addContainer(material_container) # Get quality_changes and user profiles saved in the workspace - instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] - instance_container_files = [name for name in cura_file_names if name.endswith(instance_container_suffix)] + instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] user_instance_containers = [] quality_changes_instance_containers = [] for instance_container_file in instance_container_files: - container_id = instance_container_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % instance_container_suffix, "") + container_id = self._stripFileToId(instance_container_file) instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) container_type = instance_container.getMetaDataEntry("type") if container_type == "user": + # Check if quality changes already exists. + user_containers = self._container_registry.findInstanceContainers(id=container_id) + if not user_containers: + self._container_registry.addContainer(instance_container) user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. - quality_changes = container_registry.findInstanceContainers(id = container_id) + quality_changes = self._container_registry.findInstanceContainers(id = container_id) if not quality_changes: - container_registry.addContainer(instance_container) + self._container_registry.addContainer(instance_container) quality_changes_instance_containers.append(instance_container) else: continue - # Get the stack(s) saved in the workspace. - '''container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] - container_stack_files = [name for name in cura_file_names if name.endswith(container_stack_suffix)] + container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] global_stack = None extruder_stacks = [] + for container_stack_file in container_stack_files: - container_id = container_stack_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % container_stack_suffix, "") - - # Check if a stack by this ID already exists; - container_stacks = container_registry.findContainerStacks(id = container_id) - if container_stacks: - print("CONTAINER ALREADY EXISTSSS") - - #stack = ContainerStack(container_id) + container_id = self._stripFileToId(container_stack_file) + stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Check if a stack by this ID already exists; + container_stacks = self._container_registry.findContainerStacks(id = container_id) + if container_stacks: + print("CONTAINER ALREADY EXISTSSS") + if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: - global_stack = stack''' + global_stack = stack return nodes + + def _stripFileToId(self, file): + return file.replace("Cura/", "").split(".")[0] + + def _getXmlProfileClass(self): + for type_name, container_type in self._container_registry.getContainerTypes(): + print(type_name, container_type) + if type_name == "XmlMaterialProfile": + return container_type diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py new file mode 100644 index 0000000000..ae1280b4bd --- /dev/null +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -0,0 +1,79 @@ +from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtQml import QQmlComponent, QQmlContext +from UM.PluginRegistry import PluginRegistry +from UM.Application import Application + +import os +import threading + +class WorkspaceDialog(QObject): + showDialogSignal = pyqtSignal() + + def __init__(self, parent = None): + super().__init__(parent) + self._component = None + self._context = None + self._view = None + self._qml_url = "WorkspaceDialog.qml" + self._lock = threading.Lock() + self._result = None # What option did the user pick? + self._visible = False + self.showDialogSignal.connect(self.__show) + + def getResult(self): + return self._result + + def _createViewFromQML(self): + path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)) + self._component = QQmlComponent(Application.getInstance()._engine, path) + self._context = QQmlContext(Application.getInstance()._engine.rootContext()) + self._context.setContextProperty("manager", self) + self._view = self._component.create(self._context) + + def show(self): + # Emit signal so the right thread actually shows the view. + self._lock.acquire() + self._result = None + self._visible = True + self.showDialogSignal.emit() + + @pyqtSlot() + ## Used to notify the dialog so the lock can be released. + def notifyClosed(self): + if self._result is None: + self._result = "cancel" + self._lock.release() + + def hide(self): + self._visible = False + self._lock.release() + self._view.hide() + + @pyqtSlot() + def onOverrideButtonClicked(self): + self._view.hide() + self.hide() + self._result = "override" + + @pyqtSlot() + def onNewButtonClicked(self): + self._view.hide() + self.hide() + self._result = "new" + + @pyqtSlot() + def onCancelButtonClicked(self): + self._view.hide() + self.hide() + self._result = "cancel" + + ## Block thread until the dialog is closed. + def waitForClose(self): + if self._visible: + self._lock.acquire() + self._lock.release() + + def __show(self): + if self._view is None: + self._createViewFromQML() + self._view.show() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml new file mode 100644 index 0000000000..0c56dbcb6c --- /dev/null +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -0,0 +1,52 @@ +// Copyright (c) 2016 Ultimaker B.V. +// Cura is released under the terms of the AGPLv3 or higher. + +import QtQuick 2.1 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 + +import UM 1.1 as UM + +UM.Dialog +{ + title: catalog.i18nc("@title:window", "Conflict") + + width: 350 * Screen.devicePixelRatio; + minimumWidth: 350 * Screen.devicePixelRatio; + maximumWidth: 350 * Screen.devicePixelRatio; + + height: 250 * Screen.devicePixelRatio; + minimumHeight: 250 * Screen.devicePixelRatio; + maximumHeight: 250 * Screen.devicePixelRatio; + + onClosing: manager.notifyClosed() + + Item + { + UM.I18nCatalog { id: catalog; name: "cura"; } + } + rightButtons: [ + Button + { + id: override_button + text: catalog.i18nc("@action:button","Override"); + onClicked: { manager.onOverrideButtonClicked() } + enabled: true + }, + Button + { + id: create_new + text: catalog.i18nc("@action:button","Create new"); + onClicked: { manager.onNewButtonClicked() } + enabled: true + }, + Button + { + id: cancel_button + text: catalog.i18nc("@action:button","Cancel"); + onClicked: { manager.onCancelButtonClicked() } + enabled: true + } + ] +} \ No newline at end of file From b467f0a8d4b1b7b9af91225f46f08140ed162fcf Mon Sep 17 00:00:00 2001 From: Filip Goc Date: Sun, 13 Nov 2016 11:33:49 -0500 Subject: [PATCH 320/438] add JellyBOX machine definition file gantry height left to default to effectively disable one at a time printing for now --- resources/definitions/jellybox.def.json | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 resources/definitions/jellybox.def.json diff --git a/resources/definitions/jellybox.def.json b/resources/definitions/jellybox.def.json new file mode 100644 index 0000000000..1319accabc --- /dev/null +++ b/resources/definitions/jellybox.def.json @@ -0,0 +1,35 @@ +{ + "id": "jellybox", + "version": 2, + "name": "JellyBOX", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "IMADE3D", + "manufacturer": "IMADE3D", + "category": "Other", + "platform": "jellybox_platform.stl", + "platform_offset": [ 0, 0, 0], + "file_formats": "text/x-gcode", + "has_materials": true, + "has_machine_materials": true + }, + + "overrides": { + "machine_name": { "default_value": "JellyBOX" }, + "machine_width": { "default_value": 170 }, + "machine_height": { "default_value": 145 }, + "machine_depth": { "default_value": 160 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 }, + "machine_heated_bed": { "default_value": true }, + "machine_center_is_zero": { "default_value": false }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "---------------------------------------\n; ; ; Jellybox Start Script Begin ; ; ;\n_______________________________________\n; M92 E140 ;optionally adjust steps per mm for your filament\n\n; Print Settings Summary\n; (overwriting these values will NOT change your printer's behavior)\n; sliced for: {machine_name}\n; nozzle diameter: {machine_nozzle_size}\n; filament diameter: {material_diameter}\n; layer height: {layer_height}\n; 1st layer height: {layer_height_0}\n; line width: {line_width}\n; wall thickness: {wall_thickness}\n; infill density: {infill_sparse_density}\n; infill pattern: {infill_pattern}\n; print temperature: {material_print_temperature}\n; heated bed temperature: {material_bed_temperature}\n; regular fan speed: {cool_fan_speed_min}\n; max fan speed: {cool_fan_speed_max}\n; support? {support_enable}\n; spiralized? {magic_spiralize}\n\nM117 Preparing ;write Preparing\nM140 S{material_bed_temperature} ;set bed temperature and move on\nM104 S{material_print_temperature} ;set extruder temperature and move on\nM206 X10.0 Y0.0 ;set x homing offset for default bed leveling\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nM82 ;set extruder to absolute mode\nG28 ;home all axes\nM203 Z5 ;slow Z speed down for greater accuracy when probing\nG29 ;auto bed leveling procedure\nM203 Z7 ;pick up z speed again for printing\nM190 S{material_bed_temperature} ;wait for the bed to reach desired temperature\nM109 S{material_print_temperature} ;wait for the extruder to reach desired temperature\nG92 E0 ;reset the extruder position\nG1 F200 E5 ;extrude 5mm of feed stock\nG92 E0 ;reset the extruder position again\nM117 Print starting ;write Print starting\n---------------------------------------------\n; ; ; Jellybox Printer Start Script End ; ; ;\n_____________________________________________" + }, + "machine_end_gcode": { + "default_value": "---------------------------------\n;;; Jellybox End Script Begin ;;;\n_________________________________\nM117 Finishing Up ;write Finishing Up\n\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min end stops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\n\n\nM117 Print finished ;write Print finished\n---------------------------------------\n;;; Jellybox Printer End Script End ;;;\n_______________________________________" + } + } +} From 644fb0ca00f53831dfb9eaf7f4cbf40638f8e521 Mon Sep 17 00:00:00 2001 From: Filip Goc Date: Sun, 13 Nov 2016 11:34:24 -0500 Subject: [PATCH 321/438] add JellyBOX build plate mesh --- resources/meshes/jellybox_platform.stl | Bin 0 -> 601584 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/meshes/jellybox_platform.stl diff --git a/resources/meshes/jellybox_platform.stl b/resources/meshes/jellybox_platform.stl new file mode 100644 index 0000000000000000000000000000000000000000..900c2675381f4b4a464ce98bbe4d9d7ca9f6b726 GIT binary patch literal 601584 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ð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

<@p|6rnj|Kobk$w!VVjt!5}&0i@E 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 Date: Mon, 14 Nov 2016 12:37:26 +0100 Subject: [PATCH 322/438] Override resolve strategy is now handled CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index e4a2c574ac..89260193b2 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -29,7 +29,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] - self._resolvement_strategy = None + self._resolve_strategy = None def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -69,13 +69,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): conflict = True if conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort - self._resolvement_strategy = None + self._resolve_strategy = None self._dialog.show() self._dialog.waitForClose() if self._dialog.getResult() == "cancel": return WorkspaceReader.PreReadResult.cancelled - self._resolvement_strategy = self._dialog.getResult() + self._resolve_strategy = self._dialog.getResult() pass return WorkspaceReader.PreReadResult.accepted @@ -144,12 +144,18 @@ class ThreeMFWorkspaceReader(WorkspaceReader): user_containers = self._container_registry.findInstanceContainers(id=container_id) if not user_containers: self._container_registry.addContainer(instance_container) + else: + if self._resolve_strategy == "override": + user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers(id = container_id) if not quality_changes: self._container_registry.addContainer(instance_container) + else: + if self._resolve_strategy == "override": + quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -163,13 +169,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_id = self._stripFileToId(container_stack_file) stack = ContainerStack(container_id) - # Deserialize stack by converting read data from bytes to string - stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - # Check if a stack by this ID already exists; - container_stacks = self._container_registry.findContainerStacks(id = container_id) + container_stacks = self._container_registry.findContainerStacks(id=container_id) if container_stacks: - print("CONTAINER ALREADY EXISTSSS") + if self._resolve_strategy == "override": + container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) + else: + # Deserialize stack by converting read data from bytes to string + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + self._container_registry.addContainer(stack) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) From 8ae0cfd8488f1d6e1be439ed15e4d244825d2316 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 13:30:28 +0100 Subject: [PATCH 323/438] Loading workspace now activates the machine and notifies everyone that it was changed CURA-1263 --- cura/Settings/MachineManager.py | 2 +- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index fce82212cd..ed9066c4ba 100644 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -277,7 +277,7 @@ class MachineManager(QObject): def _onInstanceContainersChanged(self, container): container_type = container.getMetaDataEntry("type") - + if container_type == "material": self.activeMaterialChanged.emit() elif container_type == "variant": diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 89260193b2..8930e42e93 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -147,6 +147,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategy == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) + else: + # TODO: Handle other resolve strategies + pass user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. @@ -168,22 +171,37 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) - stack = ContainerStack(container_id) + # Check if a stack by this ID already exists; container_stacks = self._container_registry.findContainerStacks(id=container_id) if container_stacks: + stack = container_stacks[0] if self._resolve_strategy == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) + else: + # TODO: Handle other resolve strategies + pass else: + stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + self._container_registry.addContainer(stack) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: global_stack = stack + # Notify everything/one that is to notify about changes. + for container in global_stack.getContainers(): + global_stack.containersChanged.emit(container) + for stack in extruder_stacks: + for container in stack.getContainers(): + stack.containersChanged.emit(container) + + # Actually change the active machine. + Application.getInstance().setGlobalContainerStack(global_stack) return nodes def _stripFileToId(self, file): From 377752397f5fc14ab082358939eafd4c8af203c6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 13:48:49 +0100 Subject: [PATCH 324/438] Made it possible for machine & quality changes to have different resolve strategies CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 8930e42e93..819fe1586b 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -29,7 +29,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] - self._resolve_strategy = None + self._resolve_strategies = {} def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -69,13 +69,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): conflict = True if conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort - self._resolve_strategy = None + self._resolve_strategies = {} self._dialog.show() self._dialog.waitForClose() if self._dialog.getResult() == "cancel": return WorkspaceReader.PreReadResult.cancelled - - self._resolve_strategy = self._dialog.getResult() + result = self._dialog.getResult() + # TODO: In the future it could be that machine & quality changes will have different resolve strategies + self._resolve_strategies = {"machine": result, "quality_changes": result} pass return WorkspaceReader.PreReadResult.accepted @@ -145,9 +146,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not user_containers: self._container_registry.addContainer(instance_container) else: - if self._resolve_strategy == "override": + if self._resolve_strategies["machine"] == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) else: + user_containers.deserialize(archive.open(instance_container_file).read().decode("utf-8")) # TODO: Handle other resolve strategies pass user_instance_containers.append(instance_container) @@ -157,8 +159,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not quality_changes: self._container_registry.addContainer(instance_container) else: - if self._resolve_strategy == "override": + if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) + else: + quality_changes.deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -171,12 +175,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) - # Check if a stack by this ID already exists; container_stacks = self._container_registry.findContainerStacks(id=container_id) if container_stacks: stack = container_stacks[0] - if self._resolve_strategy == "override": + if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) else: # TODO: Handle other resolve strategies @@ -185,13 +188,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - self._container_registry.addContainer(stack) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: global_stack = stack + # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): global_stack.containersChanged.emit(container) From 990736b5c62208843a3f54a856239badb28979c9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 14:51:10 +0100 Subject: [PATCH 325/438] Implemented quality_changes resolve strategy This enables the creation of a new quality_changes profile if the user chose to do this CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 819fe1586b..57a81a24f0 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -162,7 +162,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) else: - quality_changes.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -195,6 +195,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: global_stack = stack + if self._resolve_strategies["quality_changes"] == "new": + # Quality changes needs to get a new ID, added to registry and to the right stacks + for container in quality_changes_instance_containers: + old_id = container.getId() + container.setName(self._container_registry.uniqueName(container.getName())) + # We're not really supposed to change the ID in normal cases, but this is an exception. + container._id = self._container_registry.uniqueName(container.getId()) + + # The container was not added yet, as it didn't have an unique ID. It does now, so add it. + self._container_registry.addContainer(container) + + # Replace the quality changes container + old_container = global_stack.findContainer({"type": "quality_changes"}) + if old_container.getId() == old_id: + quality_changes_index = global_stack.getContainerIndex(old_container) + global_stack.replaceContainer(quality_changes_index, container) + + for stack in extruder_stacks: + old_container = stack.findContainer({"type": "quality_changes"}) + if old_container.getId() == old_id: + quality_changes_index = stack.getContainerIndex(old_container) + stack.replaceContainer(quality_changes_index, container) + # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): global_stack.containersChanged.emit(container) From 0d4f28b310c1eeda0912db6777608d11647f0bce Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:16:17 +0100 Subject: [PATCH 326/438] MachineStacks & user containers are now also renamed if so required CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 57a81a24f0..091528a4eb 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -148,8 +148,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategies["machine"] == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) - else: - user_containers.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + elif self._resolve_strategies["machine"] == "new": + # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. + extruder_id = instance_container.getMetaDataEntry("extruder", None) + if extruder_id: + new_id = self._container_registry.uniqueName(extruder_id) + "_current_settings" + instance_container._id = new_id + instance_container.setName(new_id) + self._container_registry.addContainer(instance_container) + continue + + machine_id = instance_container.getMetaDataEntry("machine", None) + if machine_id: + new_id = self._container_registry.uniqueName(machine_id) + "_current_settings" + instance_container._id = new_id + instance_container.setName(new_id) + self._container_registry.addContainer(instance_container) # TODO: Handle other resolve strategies pass user_instance_containers.append(instance_container) @@ -182,8 +196,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) else: - # TODO: Handle other resolve strategies - pass + new_id = self._container_registry.uniqueName(container_id) + stack = ContainerStack(new_id) + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Ensure a unique ID and name + stack._id = new_id + stack.setName(self._container_registry.uniqueName(stack.getName())) + self._container_registry.addContainer(stack) else: stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string @@ -211,6 +230,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if old_container.getId() == old_id: quality_changes_index = global_stack.getContainerIndex(old_container) global_stack.replaceContainer(quality_changes_index, container) + continue for stack in extruder_stacks: old_container = stack.findContainer({"type": "quality_changes"}) @@ -235,6 +255,5 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def _getXmlProfileClass(self): for type_name, container_type in self._container_registry.getContainerTypes(): - print(type_name, container_type) if type_name == "XmlMaterialProfile": return container_type From 4dc14a72ab557694e18b9b2c2709d9152fabbf6c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:32:08 +0100 Subject: [PATCH 327/438] Added conversion table for old to new ID's CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 27 +++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 091528a4eb..0933c1be50 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -31,6 +31,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._resolve_strategies = {} + self._id_mapping = {} + + ## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. + # This has nothing to do with speed, but with getting consistent new naming for instances & objects. + def getNewId(self, old_id): + if old_id not in self._id_mapping: + self._id_mapping[old_id] = self._container_registry.uniqueName(old_id) + return self._id_mapping[old_id] + def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted: @@ -101,10 +110,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): global_preferences.setValue("cura/categories_expanded", temp_preferences.getValue("cura/categories_expanded")) Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change + self._id_mapping = {} + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. - + Logger.log("d", "Workspace loading is checking definitions...") # Get all the definition files & check if they exist. If not, add them. definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] for definition_container_file in definition_container_files: @@ -115,6 +126,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) self._container_registry.addContainer(definition_container) + Logger.log("d", "Workspace loading is checking materials...") # Get all the material files and check if they exist. If not, add them. xml_material_profile = self._getXmlProfileClass() if self._material_container_suffix is None: @@ -129,6 +141,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) + Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] user_instance_containers = [] @@ -152,7 +165,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. extruder_id = instance_container.getMetaDataEntry("extruder", None) if extruder_id: - new_id = self._container_registry.uniqueName(extruder_id) + "_current_settings" + new_id = self.getNewId(extruder_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) self._container_registry.addContainer(instance_container) @@ -160,7 +173,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: - new_id = self._container_registry.uniqueName(machine_id) + "_current_settings" + new_id = self.getNewId(machine_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) self._container_registry.addContainer(instance_container) @@ -182,10 +195,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): continue # Get the stack(s) saved in the workspace. + Logger.log("d", "Workspace loading is checking stacks containers...") container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] global_stack = None extruder_stacks = [] - for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) @@ -196,7 +209,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) else: - new_id = self._container_registry.uniqueName(container_id) + new_id = self.getNewId(container_id) stack = ContainerStack(new_id) stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) # Ensure a unique ID and name @@ -220,7 +233,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): old_id = container.getId() container.setName(self._container_registry.uniqueName(container.getName())) # We're not really supposed to change the ID in normal cases, but this is an exception. - container._id = self._container_registry.uniqueName(container.getId()) + container._id = self.getNewId(container.getId()) # The container was not added yet, as it didn't have an unique ID. It does now, so add it. self._container_registry.addContainer(container) @@ -238,6 +251,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): global_stack.containersChanged.emit(container) @@ -245,7 +259,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in extruder_stacks: for container in stack.getContainers(): stack.containersChanged.emit(container) - # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) return nodes From c919883178217a093f2692949306349c06397615 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:50:07 +0100 Subject: [PATCH 328/438] Extruder stacks now properly get global stack set as next CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 0933c1be50..1f9b042d91 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -188,8 +188,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) - else: - instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -257,8 +255,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): global_stack.containersChanged.emit(container) for stack in extruder_stacks: + stack.setNextStack(global_stack) for container in stack.getContainers(): stack.containersChanged.emit(container) + # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) return nodes From b8746aee30a088340bec865341ced6f30df16d74 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:59:14 +0100 Subject: [PATCH 329/438] Added hack so the new extruders are added to extruder manager CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 1f9b042d91..05b850efcd 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -10,6 +10,9 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Preferences import Preferences from .WorkspaceDialog import WorkspaceDialog + +from cura.Settings.ExtruderManager import ExtruderManager + import zipfile import io @@ -249,6 +252,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + for stack in extruder_stacks: + if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: + ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} + #TODO: This is nasty hack; this should be made way more robust (setter?) + ExtruderManager.getInstance()._extruder_trains[global_stack.getId()][stack.getMetaDataEntry("position")] = stack + Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): From 2e4b430cf8c738f35ebb790653e1d9236fb8d6e0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:04:58 +0100 Subject: [PATCH 330/438] User changes are now also updated correctly when resolving to new machine in workspace loading CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 29 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 05b850efcd..94394ba4e1 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -171,17 +171,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader): new_id = self.getNewId(extruder_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) + instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id)) self._container_registry.addContainer(instance_container) - continue machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: new_id = self.getNewId(machine_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) + instance_container.setMetaDataEntry("machine", self.getNewId(machine_id)) self._container_registry.addContainer(instance_container) - # TODO: Handle other resolve strategies - pass user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. @@ -209,14 +208,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): stack = container_stacks[0] if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) - else: + elif self._resolve_strategies["machine"] == "new": new_id = self.getNewId(container_id) stack = ContainerStack(new_id) stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + + # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the + # bound machine also needs to change. + if stack.getMetaDataEntry("machine", None): + stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) # Ensure a unique ID and name stack._id = new_id stack.setName(self._container_registry.uniqueName(stack.getName())) self._container_registry.addContainer(stack) + else: + Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) else: stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string @@ -228,6 +234,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: global_stack = stack + if self._resolve_strategies["machine"] == "new": + # A new machine was made, but it was serialized with the wrong user container. Fix that now. + for container in user_instance_containers: + extruder_id = container.getMetaDataEntry("extruder", None) + if extruder_id: + for extruder in extruder_stacks: + if extruder.getId() == extruder_id: + extruder.replaceContainer(0, container) + continue + machine_id = container.getMetaDataEntry("machine", None) + if machine_id: + if global_stack.getId() == machine_id: + global_stack.replaceContainer(0, container) + continue + if self._resolve_strategies["quality_changes"] == "new": # Quality changes needs to get a new ID, added to registry and to the right stacks for container in quality_changes_instance_containers: From 3245c2fe32d93ab77735210ac91e5c9c0c38098d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:08:43 +0100 Subject: [PATCH 331/438] Extruder stacks are no longer renamed when loading from workspace CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 94394ba4e1..08da848392 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -213,13 +213,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): stack = ContainerStack(new_id) stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Ensure a unique ID and name + stack._id = new_id + # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the # bound machine also needs to change. if stack.getMetaDataEntry("machine", None): stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) - # Ensure a unique ID and name - stack._id = new_id - stack.setName(self._container_registry.uniqueName(stack.getName())) + + if stack.getMetaDataEntry("type") != "extruder_train": + # Only machines need a new name, stacks may be non-unique + stack.setName(self._container_registry.uniqueName(stack.getName())) self._container_registry.addContainer(stack) else: Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) From 1db2d06e0655ae34cc8030527158397e855e55a0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:15:29 +0100 Subject: [PATCH 332/438] Fixed "new" resolve if only the machine was double. CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 43 ++++++++++++--------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 08da848392..3d5ef22f0a 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -56,30 +56,31 @@ class ThreeMFWorkspaceReader(WorkspaceReader): cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] - conflict = False + machine_conflict = False + quality_changes_conflict = False for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) stacks = self._container_registry.findContainerStacks(id=container_id) if stacks: - conflict = True + machine_conflict = True break # Check if any quality_changes instance container is in conflict. - if not conflict: - instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] - for instance_container_file in instance_container_files: - container_id = self._stripFileToId(instance_container_file) - instance_container = InstanceContainer(container_id) + instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] + for instance_container_file in instance_container_files: + container_id = self._stripFileToId(instance_container_file) + instance_container = InstanceContainer(container_id) - # Deserialize InstanceContainer by converting read data from bytes to string - instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) - container_type = instance_container.getMetaDataEntry("type") - if container_type == "quality_changes": - # Check if quality changes already exists. - quality_changes = self._container_registry.findInstanceContainers(id = container_id) - if quality_changes: - conflict = True - if conflict: + # Deserialize InstanceContainer by converting read data from bytes to string + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + container_type = instance_container.getMetaDataEntry("type") + if container_type == "quality_changes": + # Check if quality changes already exists. + quality_changes = self._container_registry.findInstanceContainers(id = container_id) + if quality_changes: + quality_changes_conflict = True + + if machine_conflict or quality_changes_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort self._resolve_strategies = {} self._dialog.show() @@ -87,9 +88,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == "cancel": return WorkspaceReader.PreReadResult.cancelled result = self._dialog.getResult() - # TODO: In the future it could be that machine & quality changes will have different resolve strategies - self._resolve_strategies = {"machine": result, "quality_changes": result} - pass + + self._resolve_strategies = {"machine": None, "quality_changes": None} + if machine_conflict: + self._resolve_strategies["machine"] = result + if quality_changes_conflict: + self._resolve_strategies["quality_changes"] = result + return WorkspaceReader.PreReadResult.accepted def read(self, file_name): From 8640b2b787d78c8e54b344ceeb9e4af775fe1314 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:45:30 +0100 Subject: [PATCH 333/438] Saving an empty buildplate as a workspace is now possible CURA-1263 --- plugins/3MFWriter/ThreeMFWriter.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index acf1421655..d86b119276 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -57,10 +57,6 @@ class ThreeMFWriter(MeshWriter): return self._archive def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): - try: - MeshWriter._meshNodes(nodes).__next__() - except StopIteration: - return False #Don't write anything if there is no mesh data. self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: @@ -86,7 +82,7 @@ class ThreeMFWriter(MeshWriter): build = ET.SubElement(model, "build") added_nodes = [] - + index = 0 # Ensure index always exists (even if there are no nodes to write) # Write all nodes with meshData to the file as objects inside the resource tag for index, n in enumerate(MeshWriter._meshNodes(nodes)): added_nodes.append(n) # Save the nodes that have mesh data From bade9e1bff66983b7c50bc038bc3216aff67e7cf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:50:54 +0100 Subject: [PATCH 334/438] Resolve strategy is now always set CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 3d5ef22f0a..266217f98b 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -55,7 +55,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] - + self._resolve_strategies = {"machine": None, "quality_changes": None} machine_conflict = False quality_changes_conflict = False for container_stack_file in container_stack_files: @@ -89,7 +89,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.cancelled result = self._dialog.getResult() - self._resolve_strategies = {"machine": None, "quality_changes": None} if machine_conflict: self._resolve_strategies["machine"] = result if quality_changes_conflict: From dffb54d9010288edf3693ece6f1a3af4b9c04859 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:54:38 +0100 Subject: [PATCH 335/438] Removed abundant whitespace --- cura/Settings/ExtruderManager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index d3005a78fe..81a58c3f78 100644 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -287,7 +287,6 @@ class ExtruderManager(QObject): result.append(stack.getProperty(setting_key, property)) return result - ## Removes the container stack and user profile for the extruders for a specific machine. # # \param machine_id The machine to remove the extruders for. From 47d0e95e53907e27c33ac1748fff32de6163a520 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:55:30 +0100 Subject: [PATCH 336/438] Loading single extrusion machine from file no longer gives exception CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 266217f98b..42a9ad67f6 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -281,10 +281,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + # TODO: This is nasty hack; this should be made way more robust (setter?) + if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: + ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} for stack in extruder_stacks: - if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: - ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} - #TODO: This is nasty hack; this should be made way more robust (setter?) ExtruderManager.getInstance()._extruder_trains[global_stack.getId()][stack.getMetaDataEntry("position")] = stack Logger.log("d", "Workspace loading is notifying rest of the code of changes...") From 8d3c0a7e739d1925b9aedb91bf1ff0c9180ff8e5 Mon Sep 17 00:00:00 2001 From: Filip Goc Date: Mon, 14 Nov 2016 11:55:58 -0500 Subject: [PATCH 337/438] Fixed mistake in end gcode script The bed assembly was homing instead of going forward a little bit - which makes the print more accessible when finished. Fixed. --- resources/definitions/jellybox.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/jellybox.def.json b/resources/definitions/jellybox.def.json index 1319accabc..23569cc1ef 100644 --- a/resources/definitions/jellybox.def.json +++ b/resources/definitions/jellybox.def.json @@ -29,7 +29,7 @@ "default_value": "---------------------------------------\n; ; ; Jellybox Start Script Begin ; ; ;\n_______________________________________\n; M92 E140 ;optionally adjust steps per mm for your filament\n\n; Print Settings Summary\n; (overwriting these values will NOT change your printer's behavior)\n; sliced for: {machine_name}\n; nozzle diameter: {machine_nozzle_size}\n; filament diameter: {material_diameter}\n; layer height: {layer_height}\n; 1st layer height: {layer_height_0}\n; line width: {line_width}\n; wall thickness: {wall_thickness}\n; infill density: {infill_sparse_density}\n; infill pattern: {infill_pattern}\n; print temperature: {material_print_temperature}\n; heated bed temperature: {material_bed_temperature}\n; regular fan speed: {cool_fan_speed_min}\n; max fan speed: {cool_fan_speed_max}\n; support? {support_enable}\n; spiralized? {magic_spiralize}\n\nM117 Preparing ;write Preparing\nM140 S{material_bed_temperature} ;set bed temperature and move on\nM104 S{material_print_temperature} ;set extruder temperature and move on\nM206 X10.0 Y0.0 ;set x homing offset for default bed leveling\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nM82 ;set extruder to absolute mode\nG28 ;home all axes\nM203 Z5 ;slow Z speed down for greater accuracy when probing\nG29 ;auto bed leveling procedure\nM203 Z7 ;pick up z speed again for printing\nM190 S{material_bed_temperature} ;wait for the bed to reach desired temperature\nM109 S{material_print_temperature} ;wait for the extruder to reach desired temperature\nG92 E0 ;reset the extruder position\nG1 F200 E5 ;extrude 5mm of feed stock\nG92 E0 ;reset the extruder position again\nM117 Print starting ;write Print starting\n---------------------------------------------\n; ; ; Jellybox Printer Start Script End ; ; ;\n_____________________________________________" }, "machine_end_gcode": { - "default_value": "---------------------------------\n;;; Jellybox End Script Begin ;;;\n_________________________________\nM117 Finishing Up ;write Finishing Up\n\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min end stops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\n\n\nM117 Print finished ;write Print finished\n---------------------------------------\n;;; Jellybox Printer End Script End ;;;\n_______________________________________" + "default_value": "\n---------------------------------\n;;; Jellybox End Script Begin ;;;\n_________________________________\nM117 Finishing Up ;write Finishing Up\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nG28 X ;home x, so the head is out of the way\nG1 Y100 ;move Y forward, so the print is more accessible\nM84 ;steppers off\n\nM117 Print finished ;write Print finished\n---------------------------------------\n;;; Jellybox Printer End Script End ;;;\n_______________________________________" } } } From 006e5bf585e1a29faecc3c52d882f10b054e76ea Mon Sep 17 00:00:00 2001 From: Filip Goc Date: Mon, 14 Nov 2016 12:10:48 -0500 Subject: [PATCH 338/438] small typographic fix --- resources/definitions/jellybox.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/jellybox.def.json b/resources/definitions/jellybox.def.json index 23569cc1ef..432cf5a054 100644 --- a/resources/definitions/jellybox.def.json +++ b/resources/definitions/jellybox.def.json @@ -29,7 +29,7 @@ "default_value": "---------------------------------------\n; ; ; Jellybox Start Script Begin ; ; ;\n_______________________________________\n; M92 E140 ;optionally adjust steps per mm for your filament\n\n; Print Settings Summary\n; (overwriting these values will NOT change your printer's behavior)\n; sliced for: {machine_name}\n; nozzle diameter: {machine_nozzle_size}\n; filament diameter: {material_diameter}\n; layer height: {layer_height}\n; 1st layer height: {layer_height_0}\n; line width: {line_width}\n; wall thickness: {wall_thickness}\n; infill density: {infill_sparse_density}\n; infill pattern: {infill_pattern}\n; print temperature: {material_print_temperature}\n; heated bed temperature: {material_bed_temperature}\n; regular fan speed: {cool_fan_speed_min}\n; max fan speed: {cool_fan_speed_max}\n; support? {support_enable}\n; spiralized? {magic_spiralize}\n\nM117 Preparing ;write Preparing\nM140 S{material_bed_temperature} ;set bed temperature and move on\nM104 S{material_print_temperature} ;set extruder temperature and move on\nM206 X10.0 Y0.0 ;set x homing offset for default bed leveling\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nM82 ;set extruder to absolute mode\nG28 ;home all axes\nM203 Z5 ;slow Z speed down for greater accuracy when probing\nG29 ;auto bed leveling procedure\nM203 Z7 ;pick up z speed again for printing\nM190 S{material_bed_temperature} ;wait for the bed to reach desired temperature\nM109 S{material_print_temperature} ;wait for the extruder to reach desired temperature\nG92 E0 ;reset the extruder position\nG1 F200 E5 ;extrude 5mm of feed stock\nG92 E0 ;reset the extruder position again\nM117 Print starting ;write Print starting\n---------------------------------------------\n; ; ; Jellybox Printer Start Script End ; ; ;\n_____________________________________________" }, "machine_end_gcode": { - "default_value": "\n---------------------------------\n;;; Jellybox End Script Begin ;;;\n_________________________________\nM117 Finishing Up ;write Finishing Up\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nG28 X ;home x, so the head is out of the way\nG1 Y100 ;move Y forward, so the print is more accessible\nM84 ;steppers off\n\nM117 Print finished ;write Print finished\n---------------------------------------\n;;; Jellybox Printer End Script End ;;;\n_______________________________________" + "default_value": "\n---------------------------------\n;;; Jellybox End Script Begin ;;;\n_________________________________\nM117 Finishing Up ;write Finishing Up\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nG28 X ;home x, so the head is out of the way\nG1 Y100 ;move Y forward, so the print is more accessible\nM84 ;steppers off\n\nM117 Print finished ;write Print finished\n---------------------------------------\n;;; Jellybox End Script End ;;;\n_______________________________________" } } } From 829f8893e2d682d19d0c1739beab05d273631ee1 Mon Sep 17 00:00:00 2001 From: Victor Larchenko Date: Fri, 11 Nov 2016 13:28:00 +0600 Subject: [PATCH 339/438] T562: Added searching of settings # Conflicts: # resources/qml/Settings/SettingView.qml --- resources/qml/Settings/SettingView.qml | 452 +++++++++++++------------ 1 file changed, 241 insertions(+), 211 deletions(-) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index c47abf3ee2..d47c06053c 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -9,229 +9,259 @@ import QtQuick.Layouts 1.1 import UM 1.2 as UM import Cura 1.0 as Cura -ScrollView +Item { id: base; - style: UM.Theme.styles.scrollview; - flickableItem.flickableDirection: Flickable.VerticalFlick; - property Action configureSettings; signal showTooltip(Item item, point location, string text); signal hideTooltip(); - ListView + TextField { - id: contents - spacing: UM.Theme.getSize("default_lining").height; - cacheBuffer: 1000000; // Set a large cache to effectively just cache every list item. + id: filter; + visible: !monitoringPrint - model: UM.SettingDefinitionsModel { - id: definitionsModel; - containerId: Cura.MachineManager.activeDefinitionId - visibilityHandler: UM.SettingPreferenceVisibilityHandler { } - exclude: ["machine_settings", "command_line_settings", "infill_mesh", "infill_mesh_order"] // TODO: infill_mesh settigns are excluded hardcoded, but should be based on the fact that settable_globally, settable_per_meshgroup and settable_per_extruder are false. - expanded: Printer.expandedCategories - onExpandedChanged: Printer.setExpandedCategories(expanded) - onVisibilityChanged: Cura.SettingInheritanceManager.forceUpdate() - } - - delegate: Loader + anchors { - id: delegate - - width: UM.Theme.getSize("sidebar").width; - height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : - contents.spacing - Behavior on height { NumberAnimation { duration: 100 } } - opacity: provider.properties.enabled == "True" ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: 100 } } - enabled: - { - if(!ExtruderManager.activeExtruderStackId && ExtruderManager.extruderCount > 0) - { - // disable all controls on the global tab, except categories - return model.type == "category" - } - return provider.properties.enabled == "True" - } - - property var definition: model - property var settingDefinitionsModel: definitionsModel - property var propertyProvider: provider - property var globalPropertyProvider: inheritStackProvider - - //Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989 - //In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes, - //causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely. - asynchronous: model.type != "enum" && model.type != "extruder" - active: model.type != undefined - - source: - { - switch(model.type) - { - case "int": - return "SettingTextField.qml" - case "float": - return "SettingTextField.qml" - case "enum": - return "SettingComboBox.qml" - case "extruder": - return "SettingExtruder.qml" - case "bool": - return "SettingCheckBox.qml" - case "str": - return "SettingTextField.qml" - case "category": - return "SettingCategory.qml" - default: - return "SettingUnknown.qml" - } - } - - // Binding to ensure that the right containerstack ID is set for the provider. - // This ensures that if a setting has a limit_to_extruder id (for instance; Support speed points to the - // extruder that actually prints the support, as that is the setting we need to use to calculate the value) - Binding - { - target: provider - property: "containerStackId" - when: model.settable_per_extruder || (inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0); - value: - { - if(!model.settable_per_extruder || machineExtruderCount.properties.value == 1) - { - //Not settable per extruder or there only is global, so we must pick global. - return Cura.MachineManager.activeMachineId; - } - if(inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0) - { - //We have limit_to_extruder, so pick that stack. - return ExtruderManager.extruderIds[String(inheritStackProvider.properties.limit_to_extruder)]; - } - if(ExtruderManager.activeExtruderStackId) - { - //We're on an extruder tab. Pick the current extruder. - return ExtruderManager.activeExtruderStackId; - } - //No extruder tab is selected. Pick the global stack. Shouldn't happen any more since we removed the global tab. - return Cura.MachineManager.activeMachineId; - } - } - - // Specialty provider that only watches global_inherits (we cant filter on what property changed we get events - // so we bypass that to make a dedicated provider). - UM.SettingPropertyProvider - { - id: inheritStackProvider - containerStackId: Cura.MachineManager.activeMachineId - key: model.key - watchedProperties: [ "limit_to_extruder" ] - } - - UM.SettingPropertyProvider - { - id: provider - - containerStackId: Cura.MachineManager.activeMachineId - key: model.key ? model.key : "" - watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ] - storeIndex: 0 - // Due to the way setPropertyValue works, removeUnusedValue gives the correct output in case of resolve - removeUnusedValue: model.resolve == undefined - } - - Connections - { - target: item - onContextMenuRequested: - { - contextMenu.key = model.key; - contextMenu.provider = provider - contextMenu.popup(); - } - onShowTooltip: base.showTooltip(delegate, { x: 0, y: delegate.height / 2 }, text) - onHideTooltip: base.hideTooltip() - onShowAllHiddenInheritedSettings: - { - var children_with_override = Cura.SettingInheritanceManager.getChildrenKeysWithOverride(category_id) - for(var i = 0; i < children_with_override.length; i++) - { - definitionsModel.setVisible(children_with_override[i], true) - } - Cura.SettingInheritanceManager.manualRemoveOverride(category_id) - } - } + top: parent.top + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + //right: parent.right } - UM.I18nCatalog { id: catalog; name: "uranium"; } + placeholderText: catalog.i18nc("@label:textbox", "Filter...") - add: Transition { - SequentialAnimation { - NumberAnimation { properties: "height"; from: 0; duration: 100 } - NumberAnimation { properties: "opacity"; from: 0; duration: 100 } - } - } - remove: Transition { - SequentialAnimation { - NumberAnimation { properties: "opacity"; to: 0; duration: 100 } - NumberAnimation { properties: "height"; to: 0; duration: 100 } - } - } - addDisplaced: Transition { - NumberAnimation { properties: "x,y"; duration: 100 } - } - removeDisplaced: Transition { - SequentialAnimation { - PauseAnimation { duration: 100; } - NumberAnimation { properties: "x,y"; duration: 100 } - } - } - - Menu - { - id: contextMenu - - property string key - property var provider - - MenuItem - { - //: Settings context menu action - text: catalog.i18nc("@action:menu", "Copy value to all extruders") - visible: machineExtruderCount.properties.value > 1 - enabled: contextMenu.provider != undefined && contextMenu.provider.properties.settable_per_extruder != "False" - onTriggered: Cura.MachineManager.copyValueToExtruders(contextMenu.key) - } - - MenuSeparator - { - visible: machineExtruderCount.properties.value > 1 - } - - MenuItem - { - //: Settings context menu action - text: catalog.i18nc("@action:menu", "Hide this setting"); - onTriggered: definitionsModel.hide(contextMenu.key); - } - MenuItem - { - //: Settings context menu action - text: catalog.i18nc("@action:menu", "Configure setting visiblity..."); - - onTriggered: Cura.Actions.configureSettingVisibility.trigger(contextMenu); - } - } - - UM.SettingPropertyProvider - { - id: machineExtruderCount - - containerStackId: Cura.MachineManager.activeMachineId - key: "machine_extruder_count" - watchedProperties: [ "value" ] - storeIndex: 0 + onTextChanged: { + definitionsModel.filter = {"label": "*" + text}; + definitionsModel.expanded = text.length > 0 ? ["*"] : [""] } } -} + + ScrollView + { + anchors.top: filter.bottom; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + anchors.left: parent.left; + anchors.topMargin: UM.Theme.getSize("default_margin").width + + style: UM.Theme.styles.scrollview; + flickableItem.flickableDirection: Flickable.VerticalFlick; + + ListView + { + id: contents + spacing: UM.Theme.getSize("default_lining").height; + cacheBuffer: 1000000; // Set a large cache to effectively just cache every list item. + + model: UM.SettingDefinitionsModel { + id: definitionsModel; + containerId: Cura.MachineManager.activeDefinitionId + visibilityHandler: UM.SettingPreferenceVisibilityHandler { } + exclude: ["machine_settings", "command_line_settings", "infill_mesh", "infill_mesh_order"] // TODO: infill_mesh settigns are excluded hardcoded, but should be based on the fact that settable_globally, settable_per_meshgroup and settable_per_extruder are false. + expanded: Printer.expandedCategories + onExpandedChanged: Printer.setExpandedCategories(expanded) + onVisibilityChanged: Cura.SettingInheritanceManager.forceUpdate() + } + + delegate: Loader + { + id: delegate + + width: UM.Theme.getSize("sidebar").width; + height: provider.properties.enabled == "True" ? UM.Theme.getSize("section").height : - contents.spacing + Behavior on height { NumberAnimation { duration: 100 } } + opacity: provider.properties.enabled == "True" ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + enabled: + { + if(!ExtruderManager.activeExtruderStackId && ExtruderManager.extruderCount > 0) + { + // disable all controls on the global tab, except categories + return model.type == "category" + } + return provider.properties.enabled == "True" + } + + property var definition: model + property var settingDefinitionsModel: definitionsModel + property var propertyProvider: provider + property var globalPropertyProvider: inheritStackProvider + + //Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989 + //In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes, + //causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely. + asynchronous: model.type != "enum" && model.type != "extruder" + active: model.type != undefined + + source: + { + switch(model.type) + { + case "int": + return "SettingTextField.qml" + case "float": + return "SettingTextField.qml" + case "enum": + return "SettingComboBox.qml" + case "extruder": + return "SettingExtruder.qml" + case "bool": + return "SettingCheckBox.qml" + case "str": + return "SettingTextField.qml" + case "category": + return "SettingCategory.qml" + default: + return "SettingUnknown.qml" + } + } + + // Binding to ensure that the right containerstack ID is set for the provider. + // This ensures that if a setting has a limit_to_extruder id (for instance; Support speed points to the + // extruder that actually prints the support, as that is the setting we need to use to calculate the value) + Binding + { + target: provider + property: "containerStackId" + when: model.settable_per_extruder || (inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0); + value: + { + if(!model.settable_per_extruder || machineExtruderCount.properties.value == 1) + { + //Not settable per extruder or there only is global, so we must pick global. + return Cura.MachineManager.activeMachineId; + } + if(inheritStackProvider.properties.limit_to_extruder != null && inheritStackProvider.properties.limit_to_extruder >= 0) + { + //We have limit_to_extruder, so pick that stack. + return ExtruderManager.extruderIds[String(inheritStackProvider.properties.limit_to_extruder)]; + } + if(ExtruderManager.activeExtruderStackId) + { + //We're on an extruder tab. Pick the current extruder. + return ExtruderManager.activeExtruderStackId; + } + //No extruder tab is selected. Pick the global stack. Shouldn't happen any more since we removed the global tab. + return Cura.MachineManager.activeMachineId; + } + } + + // Specialty provider that only watches global_inherits (we cant filter on what property changed we get events + // so we bypass that to make a dedicated provider). + UM.SettingPropertyProvider + { + id: inheritStackProvider + containerStackId: Cura.MachineManager.activeMachineId + key: model.key + watchedProperties: [ "limit_to_extruder" ] + } + + UM.SettingPropertyProvider + { + id: provider + + containerStackId: Cura.MachineManager.activeMachineId + key: model.key ? model.key : "" + watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ] + storeIndex: 0 + // Due to the way setPropertyValue works, removeUnusedValue gives the correct output in case of resolve + removeUnusedValue: model.resolve == undefined + } + + Connections + { + target: item + onContextMenuRequested: + { + contextMenu.key = model.key; + contextMenu.provider = provider + contextMenu.popup(); + } + onShowTooltip: base.showTooltip(delegate, { x: 0, y: delegate.height / 2 }, text) + onHideTooltip: base.hideTooltip() + onShowAllHiddenInheritedSettings: + { + var children_with_override = Cura.SettingInheritanceManager.getChildrenKeysWithOverride(category_id) + for(var i = 0; i < children_with_override.length; i++) + { + definitionsModel.setVisible(children_with_override[i], true) + } + Cura.SettingInheritanceManager.manualRemoveOverride(category_id) + } + } + } + + UM.I18nCatalog { id: catalog; name: "uranium"; } + + add: Transition { + SequentialAnimation { + NumberAnimation { properties: "height"; from: 0; duration: 100 } + NumberAnimation { properties: "opacity"; from: 0; duration: 100 } + } + } + remove: Transition { + SequentialAnimation { + NumberAnimation { properties: "opacity"; to: 0; duration: 100 } + NumberAnimation { properties: "height"; to: 0; duration: 100 } + } + } + addDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 100 } + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: 100; } + NumberAnimation { properties: "x,y"; duration: 100 } + } + } + + Menu + { + id: contextMenu + + property string key + property var provider + + MenuItem + { + //: Settings context menu action + text: catalog.i18nc("@action:menu", "Copy value to all extruders") + visible: machineExtruderCount.properties.value > 1 + enabled: contextMenu.provider != undefined && contextMenu.provider.properties.settable_per_extruder != "False" + onTriggered: Cura.MachineManager.copyValueToExtruders(contextMenu.key) + } + + MenuSeparator + { + visible: machineExtruderCount.properties.value > 1 + } + + MenuItem + { + //: Settings context menu action + text: catalog.i18nc("@action:menu", "Hide this setting"); + onTriggered: definitionsModel.hide(contextMenu.key); + } + MenuItem + { + //: Settings context menu action + text: catalog.i18nc("@action:menu", "Configure setting visiblity..."); + + onTriggered: Cura.Actions.configureSettingVisibility.trigger(contextMenu); + } + } + + UM.SettingPropertyProvider + { + id: machineExtruderCount + + containerStackId: Cura.MachineManager.activeMachineId + key: "machine_extruder_count" + watchedProperties: [ "value" ] + storeIndex: 0 + } + } + } +} \ No newline at end of file From f7854225fa232d592f23a40cb259122d8f9211bc Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 14 Nov 2016 10:21:15 +0100 Subject: [PATCH 340/438] Restore expanded categories when clearing filter --- resources/qml/Settings/SettingView.qml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index d47c06053c..3c94d87034 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -32,9 +32,25 @@ Item placeholderText: catalog.i18nc("@label:textbox", "Filter...") + property var expandedCategories + property bool lastFilterEmpty: true + onTextChanged: { definitionsModel.filter = {"label": "*" + text}; - definitionsModel.expanded = text.length > 0 ? ["*"] : [""] + var _filterEmpty = (text.length == 0); + if(_filterEmpty != lastFilterEmpty) + { + if(!_filterEmpty) + { + expandedCategories = definitionsModel.expanded.slice(); + definitionsModel.expanded = ["*"]; + } + else + { + definitionsModel.expanded = expandedCategories; + } + lastFilterEmpty = _filterEmpty; + } } } @@ -55,7 +71,8 @@ Item spacing: UM.Theme.getSize("default_lining").height; cacheBuffer: 1000000; // Set a large cache to effectively just cache every list item. - model: UM.SettingDefinitionsModel { + model: UM.SettingDefinitionsModel + { id: definitionsModel; containerId: Cura.MachineManager.activeDefinitionId visibilityHandler: UM.SettingPreferenceVisibilityHandler { } From 479056efef2cbd0b39ad49e86aa400e0fa1d0ec0 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 14 Nov 2016 10:34:55 +0100 Subject: [PATCH 341/438] Always show categories when filtering --- resources/qml/Settings/SettingView.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 3c94d87034..fc7563399e 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -44,10 +44,12 @@ Item { expandedCategories = definitionsModel.expanded.slice(); definitionsModel.expanded = ["*"]; + definitionsModel.showAncestors = true } else { definitionsModel.expanded = expandedCategories; + definitionsModel.showAncestors = false } lastFilterEmpty = _filterEmpty; } From e450d10ddb13fe7842bb7214bb521345df943676 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 14 Nov 2016 11:57:49 +0100 Subject: [PATCH 342/438] Apply styling to filter field --- resources/qml/Settings/SettingView.qml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index fc7563399e..bc50e63b5d 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -32,6 +32,19 @@ Item placeholderText: catalog.i18nc("@label:textbox", "Filter...") + style: TextFieldStyle + { + textColor: UM.Theme.getColor("setting_control_text"); + font: UM.Theme.getFont("default"); + background: Rectangle + { + border.width: UM.Theme.getSize("default_lining").width + border.color: control.hovered ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") + + color: UM.Theme.getColor("setting_control") + } + } + property var expandedCategories property bool lastFilterEmpty: true From b63f4e0bee831eee5f3f9019215ec725612b961e Mon Sep 17 00:00:00 2001 From: Victor Larchenko Date: Mon, 14 Nov 2016 13:36:07 +0600 Subject: [PATCH 343/438] T562: Text box expanded and all settings visible while filtering # Conflicts: # resources/qml/Settings/SettingView.qml --- resources/qml/Settings/SettingView.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index bc50e63b5d..96fcb6513b 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -27,7 +27,8 @@ Item top: parent.top left: parent.left leftMargin: UM.Theme.getSize("default_margin").width - //right: parent.right + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width } placeholderText: catalog.i18nc("@label:textbox", "Filter...") @@ -57,12 +58,14 @@ Item { expandedCategories = definitionsModel.expanded.slice(); definitionsModel.expanded = ["*"]; - definitionsModel.showAncestors = true + definitionsModel.showAncestors = true; + definitionsModel.showAll = true; } else { definitionsModel.expanded = expandedCategories; - definitionsModel.showAncestors = false + definitionsModel.showAncestors = false; + definitionsModel.showAll = false; } lastFilterEmpty = _filterEmpty; } From 7fafcef40b3f3b9f371d60f27d87910294b47730 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 15 Nov 2016 11:09:58 +0100 Subject: [PATCH 344/438] Allow making settings visible from the sidebar --- resources/qml/Settings/SettingItem.qml | 1 + resources/qml/Settings/SettingView.qml | 41 ++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml index 7fa2856e27..c32247e925 100644 --- a/resources/qml/Settings/SettingItem.qml +++ b/resources/qml/Settings/SettingItem.qml @@ -117,6 +117,7 @@ Item { elide: Text.ElideMiddle; color: UM.Theme.getColor("setting_control_text"); + opacity: (definition.visible) ? 1 : 0.5 // emphasize the setting if it has a value in the user or quality profile font: base.doQualityUserSettingEmphasis && base.stackLevel != undefined && base.stackLevel <= 1 ? UM.Theme.getFont("default_italic") : UM.Theme.getFont("default") } diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 96fcb6513b..0058b6d359 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -14,6 +14,7 @@ Item id: base; property Action configureSettings; + property bool findingSettings; signal showTooltip(Item item, point location, string text); signal hideTooltip(); @@ -47,14 +48,14 @@ Item } property var expandedCategories - property bool lastFilterEmpty: true + property bool lastFindingSettings: false onTextChanged: { definitionsModel.filter = {"label": "*" + text}; - var _filterEmpty = (text.length == 0); - if(_filterEmpty != lastFilterEmpty) + findingSettings = (text.length > 0); + if(findingSettings != lastFindingSettings) { - if(!_filterEmpty) + if(findingSettings) { expandedCategories = definitionsModel.expanded.slice(); definitionsModel.expanded = ["*"]; @@ -67,7 +68,7 @@ Item definitionsModel.showAncestors = false; definitionsModel.showAll = false; } - lastFilterEmpty = _filterEmpty; + lastFindingSettings = findingSettings; } } } @@ -211,6 +212,7 @@ Item onContextMenuRequested: { contextMenu.key = model.key; + contextMenu.settingVisible = model.visible; contextMenu.provider = provider contextMenu.popup(); } @@ -258,6 +260,7 @@ Item property string key property var provider + property bool settingVisible MenuItem { @@ -276,10 +279,38 @@ Item MenuItem { //: Settings context menu action + visible: !findingSettings; text: catalog.i18nc("@action:menu", "Hide this setting"); onTriggered: definitionsModel.hide(contextMenu.key); } MenuItem + { + //: Settings context menu action + text: + { + if (contextMenu.settingVisible) + { + return catalog.i18nc("@action:menu", "Don't show this setting"); + } + else + { + return catalog.i18nc("@action:menu", "Keep this setting visible"); + } + } + visible: findingSettings; + onTriggered: + { + if (contextMenu.settingVisible) + { + definitionsModel.hide(contextMenu.key); + } + else + { + definitionsModel.show(contextMenu.key); + } + } + } + MenuItem { //: Settings context menu action text: catalog.i18nc("@action:menu", "Configure setting visiblity..."); From f0eb5e0da3f5e41d43e4b90d727b9174627fbbae Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 15 Nov 2016 15:10:10 +0100 Subject: [PATCH 345/438] User can now select what strategy to use per conflict CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 18 ++- plugins/3MFReader/WorkspaceDialog.py | 53 ++++++--- plugins/3MFReader/WorkspaceDialog.qml | 116 +++++++++++++++++--- 3 files changed, 154 insertions(+), 33 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 42a9ad67f6..31ea96d6df 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -82,17 +82,23 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if machine_conflict or quality_changes_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort - self._resolve_strategies = {} + self._dialog.setMachineConflict(machine_conflict) + self._dialog.setQualityChangesConflict(quality_changes_conflict) self._dialog.show() self._dialog.waitForClose() - if self._dialog.getResult() == "cancel": + if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled result = self._dialog.getResult() + # If there is no conflict, ignore the data. + print("beep", result) + if not machine_conflict: + result["machine"] = None + if not quality_changes_conflict: + result["quality_changes"] = None - if machine_conflict: - self._resolve_strategies["machine"] = result - if quality_changes_conflict: - self._resolve_strategies["quality_changes"] = result + + self._resolve_strategies = result + print("STRATEGY WAS", self._resolve_strategies) return WorkspaceReader.PreReadResult.accepted diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index ae1280b4bd..c90561e52d 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -1,7 +1,8 @@ -from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty from PyQt5.QtQml import QQmlComponent, QQmlContext from UM.PluginRegistry import PluginRegistry from UM.Application import Application +from UM.Logger import Logger import os import threading @@ -16,10 +17,36 @@ class WorkspaceDialog(QObject): self._view = None self._qml_url = "WorkspaceDialog.qml" self._lock = threading.Lock() - self._result = None # What option did the user pick? + self._default_strategy = "override" + self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} self._visible = False self.showDialogSignal.connect(self.__show) + self._has_quality_changes_conflict = False + self._has_machine_conflict = False + + machineConflictChanged = pyqtSignal() + qualityChangesConflictChanged = pyqtSignal() + + @pyqtProperty(bool, notify = machineConflictChanged) + def machineConflict(self): + return self._has_machine_conflict + + @pyqtProperty(bool, notify=qualityChangesConflictChanged) + def qualityChangesConflict(self): + return self._has_quality_changes_conflict + + @pyqtSlot(str, str) + def setResolveStrategy(self, key, strategy): + if key in self._result: + self._result[key] = strategy + + def setMachineConflict(self, machine_conflict): + self._has_machine_conflict = machine_conflict + + def setQualityChangesConflict(self, quality_changes_conflict): + self._has_quality_changes_conflict = quality_changes_conflict + def getResult(self): return self._result @@ -29,11 +56,15 @@ class WorkspaceDialog(QObject): self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) + if self._view is None: + Logger.log("e", "QQmlComponent status %s", self._component.status()) + Logger.log("e", "QQmlComponent errorString %s", self._component.errorString()) def show(self): # Emit signal so the right thread actually shows the view. self._lock.acquire() - self._result = None + # Reset the result + self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} self._visible = True self.showDialogSignal.emit() @@ -41,7 +72,7 @@ class WorkspaceDialog(QObject): ## Used to notify the dialog so the lock can be released. def notifyClosed(self): if self._result is None: - self._result = "cancel" + self._result = {} self._lock.release() def hide(self): @@ -50,22 +81,15 @@ class WorkspaceDialog(QObject): self._view.hide() @pyqtSlot() - def onOverrideButtonClicked(self): + def onOkButtonClicked(self): self._view.hide() self.hide() - self._result = "override" - - @pyqtSlot() - def onNewButtonClicked(self): - self._view.hide() - self.hide() - self._result = "new" @pyqtSlot() def onCancelButtonClicked(self): self._view.hide() self.hide() - self._result = "cancel" + self._result = {} ## Block thread until the dialog is closed. def waitForClose(self): @@ -76,4 +100,5 @@ class WorkspaceDialog(QObject): def __show(self): if self._view is None: self._createViewFromQML() - self._view.show() + if self._view: + self._view.show() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 0c56dbcb6c..6014739c39 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -10,7 +10,7 @@ import UM 1.1 as UM UM.Dialog { - title: catalog.i18nc("@title:window", "Conflict") + title: catalog.i18nc("@title:window", "Import workspace conflict") width: 350 * Screen.devicePixelRatio; minimumWidth: 350 * Screen.devicePixelRatio; @@ -21,24 +21,114 @@ UM.Dialog maximumHeight: 250 * Screen.devicePixelRatio; onClosing: manager.notifyClosed() - + onVisibleChanged: + { + if(visible) + { + machineResolveComboBox.currentIndex = 0 + qualityChangesResolveComboBox.currentIndex = 0 + } + } Item { - UM.I18nCatalog { id: catalog; name: "cura"; } + anchors.fill: parent + + UM.I18nCatalog + { + id: catalog; + name: "cura"; + } + + ListModel + { + id: resolveStrategiesModel + // Instead of directly adding the list elements, we add them afterwards. + // This is because it's impossible to use setting function results to be bound to listElement properties directly. + // See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties + Component.onCompleted: + { + append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Override existing")}); + append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")}); + } + } + + Column + { + anchors.fill: parent + Label + { + id: infoLabel + width: parent.width + text: catalog.i18nc("@action:label", "Cura detected a number of conflicts while importing the workspace. How would you like to resolve these?") + wrapMode: Text.Wrap + height: 50 + } + UM.TooltipArea + { + id: machineResolveTooltip + width: parent.width + height: visible ? 25 : 0 + text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?") + visible: manager.machineConflict + Row + { + width: parent.width + height: childrenRect.height + Label + { + text: catalog.i18nc("@action:label","Machine") + width: 150 + } + + ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: machineResolveComboBox + onActivated: + { + manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key) + } + } + } + } + UM.TooltipArea + { + id: qualityChangesResolveTooltip + width: parent.width + height: visible ? 25 : 0 + text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?") + visible: manager.qualityChangesConflict + Row + { + width: parent.width + height: childrenRect.height + Label + { + text: catalog.i18nc("@action:label","Profile") + width: 150 + } + + ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: qualityChangesResolveComboBox + onActivated: + { + manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key) + } + } + } + } + } } rightButtons: [ Button { - id: override_button - text: catalog.i18nc("@action:button","Override"); - onClicked: { manager.onOverrideButtonClicked() } - enabled: true - }, - Button - { - id: create_new - text: catalog.i18nc("@action:button","Create new"); - onClicked: { manager.onNewButtonClicked() } + id: ok_button + text: catalog.i18nc("@action:button","OK"); + onClicked: { manager.onOkButtonClicked() } enabled: true }, Button From bbf5c73dae3e394e2d223c3ecaf12f4445d3f7e2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 15 Nov 2016 15:11:32 +0100 Subject: [PATCH 346/438] Quality_changes user option is now set correctly CURA-1263 --- plugins/3MFReader/WorkspaceDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 6014739c39..1726b7abaa 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -116,7 +116,7 @@ UM.Dialog id: qualityChangesResolveComboBox onActivated: { - manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key) + manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key) } } } From fcd4fb86f54d59a362c39bb47e32abef5c6d3a1e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 15 Nov 2016 15:12:04 +0100 Subject: [PATCH 347/438] removed debug prints CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 31ea96d6df..f2cb4c72a7 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -88,17 +88,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.waitForClose() if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled + result = self._dialog.getResult() # If there is no conflict, ignore the data. - print("beep", result) if not machine_conflict: result["machine"] = None if not quality_changes_conflict: result["quality_changes"] = None - - self._resolve_strategies = result - print("STRATEGY WAS", self._resolve_strategies) return WorkspaceReader.PreReadResult.accepted From 1c92b9ee0e9ed29a5add6f61ec42bd9b5c052c99 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 15 Nov 2016 16:25:39 +0100 Subject: [PATCH 348/438] Don't update expandedCategories preference while filtering settings --- resources/qml/Settings/SettingView.qml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 0058b6d359..146049d814 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -97,7 +97,15 @@ Item visibilityHandler: UM.SettingPreferenceVisibilityHandler { } exclude: ["machine_settings", "command_line_settings", "infill_mesh", "infill_mesh_order"] // TODO: infill_mesh settigns are excluded hardcoded, but should be based on the fact that settable_globally, settable_per_meshgroup and settable_per_extruder are false. expanded: Printer.expandedCategories - onExpandedChanged: Printer.setExpandedCategories(expanded) + onExpandedChanged: + { + if(!findingSettings) + { + // Do not change expandedCategories preference while filtering settings + // because all categories are expanded while filtering + Printer.setExpandedCategories(expanded) + } + } onVisibilityChanged: Cura.SettingInheritanceManager.forceUpdate() } From 100e1f4f40e5fe6c9a8587ed08f5f09a1e38e962 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 13:10:42 +0100 Subject: [PATCH 349/438] Conflict in quality changes is now handled less naive Instead of only checking ID, we also check values of the QC CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 5 ++++- plugins/3MFReader/WorkspaceDialog.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index f2cb4c72a7..998fee5ccb 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -78,7 +78,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers(id = container_id) if quality_changes: - quality_changes_conflict = True + # Check if there really is a conflict by comparing the values + if quality_changes[0] != instance_container: + quality_changes_conflict = True + break if machine_conflict or quality_changes_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index c90561e52d..bbac4fb557 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -43,9 +43,11 @@ class WorkspaceDialog(QObject): def setMachineConflict(self, machine_conflict): self._has_machine_conflict = machine_conflict + self.machineConflictChanged.emit() def setQualityChangesConflict(self, quality_changes_conflict): self._has_quality_changes_conflict = quality_changes_conflict + self.qualityChangesConflictChanged.emit() def getResult(self): return self._result From b59be4c88b51c36aabca307cf4b92b3a31dbfc57 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 13:15:15 +0100 Subject: [PATCH 350/438] Moved result checking to the Dialog CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 8 +------- plugins/3MFReader/WorkspaceDialog.py | 4 ++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 998fee5ccb..0a25e5369e 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -92,13 +92,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled - result = self._dialog.getResult() - # If there is no conflict, ignore the data. - if not machine_conflict: - result["machine"] = None - if not quality_changes_conflict: - result["quality_changes"] = None - self._resolve_strategies = result + self._resolve_strategies = self._dialog.getResult() return WorkspaceReader.PreReadResult.accepted diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index bbac4fb557..8d98de05d2 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -50,6 +50,10 @@ class WorkspaceDialog(QObject): self.qualityChangesConflictChanged.emit() def getResult(self): + if "machine" in self._result and not self._has_machine_conflict: + self._result["machine"] = None + if "quality_changes" in self._result and not self._has_quality_changes_conflict: + self._result["quality_changes"] = None return self._result def _createViewFromQML(self): From b175e6876fbbd22b21d6e69e981e49604d26cb02 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 14:45:34 +0100 Subject: [PATCH 351/438] Added material conflict option This is still desabled by default due to some architecture issues (so this is temporarily left as it is) CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 22 ++++++++++++++- plugins/3MFReader/WorkspaceDialog.py | 24 ++++++++++++++--- plugins/3MFReader/WorkspaceDialog.qml | 30 +++++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 0a25e5369e..df395b9e67 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -65,6 +65,18 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_conflict = True break + material_conflict = False + xml_material_profile = self._getXmlProfileClass() + if self._material_container_suffix is None: + self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] + if xml_material_profile: + material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)] + for material_container_file in material_container_files: + container_id = self._stripFileToId(material_container_file) + materials = self._container_registry.findInstanceContainers(id=container_id) + if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict + material_conflict = True + # Check if any quality_changes instance container is in conflict. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] for instance_container_file in instance_container_files: @@ -83,10 +95,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_conflict = True break - if machine_conflict or quality_changes_conflict: + if machine_conflict or quality_changes_conflict or material_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort self._dialog.setMachineConflict(machine_conflict) self._dialog.setQualityChangesConflict(quality_changes_conflict) + self._dialog.setMaterialConflict(material_conflict) self._dialog.show() self._dialog.waitForClose() if self._dialog.getResult() == {}: @@ -147,6 +160,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container = xml_material_profile(container_id) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) + else: + if self._resolve_strategies["material"] == "override": + pass + Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace @@ -194,6 +211,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) + elif self._resolve_strategies["quality_changes"] is None: + # The ID already exists, but nothing in the values changed, so do nothing. + pass quality_changes_instance_containers.append(instance_container) else: continue diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 8d98de05d2..96a22e4cd7 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -18,15 +18,19 @@ class WorkspaceDialog(QObject): self._qml_url = "WorkspaceDialog.qml" self._lock = threading.Lock() self._default_strategy = "override" - self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} + self._result = {"machine": self._default_strategy, + "quality_changes": self._default_strategy, + "material": self._default_strategy} self._visible = False self.showDialogSignal.connect(self.__show) self._has_quality_changes_conflict = False self._has_machine_conflict = False + self._has_material_conflict = False machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() + materialConflictChanged = pyqtSignal() @pyqtProperty(bool, notify = machineConflictChanged) def machineConflict(self): @@ -36,11 +40,19 @@ class WorkspaceDialog(QObject): def qualityChangesConflict(self): return self._has_quality_changes_conflict + @pyqtProperty(bool, notify=materialConflictChanged) + def materialConflict(self): + return self._has_material_conflict + @pyqtSlot(str, str) def setResolveStrategy(self, key, strategy): if key in self._result: self._result[key] = strategy + def setMaterialConflict(self, material_conflict): + self._has_material_conflict = material_conflict + self.materialConflictChanged.emit() + def setMachineConflict(self, machine_conflict): self._has_machine_conflict = machine_conflict self.machineConflictChanged.emit() @@ -54,6 +66,8 @@ class WorkspaceDialog(QObject): self._result["machine"] = None if "quality_changes" in self._result and not self._has_quality_changes_conflict: self._result["quality_changes"] = None + if "material" in self._result and not self._has_material_conflict: + self._result["material"] = None return self._result def _createViewFromQML(self): @@ -63,14 +77,16 @@ class WorkspaceDialog(QObject): self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: - Logger.log("e", "QQmlComponent status %s", self._component.status()) - Logger.log("e", "QQmlComponent errorString %s", self._component.errorString()) + Logger.log("c", "QQmlComponent status %s", self._component.status()) + Logger.log("c", "QQmlComponent error string %s", self._component.errorString()) def show(self): # Emit signal so the right thread actually shows the view. self._lock.acquire() # Reset the result - self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} + self._result = {"machine": self._default_strategy, + "quality_changes": self._default_strategy, + "material": self._default_strategy} self._visible = True self.showDialogSignal.emit() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 1726b7abaa..3b33fa8661 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -27,6 +27,7 @@ UM.Dialog { machineResolveComboBox.currentIndex = 0 qualityChangesResolveComboBox.currentIndex = 0 + materialConflictComboBox.currentIndex = 0 } } Item @@ -121,6 +122,35 @@ UM.Dialog } } } + UM.TooltipArea + { + id: materialResolveTooltip + width: parent.width + height: visible ? 25 : 0 + text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?") + visible: false #manager.materialConflict + Row + { + width: parent.width + height: childrenRect.height + Label + { + text: catalog.i18nc("@action:label","Material") + width: 150 + } + + ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: materialResolveComboBox + onActivated: + { + manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key) + } + } + } + } } } rightButtons: [ From 053f0ca031cef47bc9889ee8f9cfb451a4b14b5a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 14:54:48 +0100 Subject: [PATCH 352/438] Replaced hack for setting extruders with more robust setter CURA-1263 --- cura/Settings/ExtruderManager.py | 12 ++++++++++++ plugins/3MFReader/ThreeMFWorkspaceReader.py | 8 ++++---- plugins/3MFReader/WorkspaceDialog.qml | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 81a58c3f78..50d2034860 100644 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -150,6 +150,18 @@ class ExtruderManager(QObject): if changed: self.extrudersChanged.emit(machine_id) + def registerExtruder(self, extruder_train, machine_id): + changed = False + + if machine_id not in self._extruder_trains: + self._extruder_trains[machine_id] = {} + changed = True + if extruder_train: + self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train + changed = True + if changed: + self.extrudersChanged.emit(machine_id) + ## Creates a container stack for an extruder train. # # The container stack has an extruder definition at the bottom, which is diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index df395b9e67..719ec1c8d7 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -301,11 +301,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) - # TODO: This is nasty hack; this should be made way more robust (setter?) - if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: - ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} for stack in extruder_stacks: - ExtruderManager.getInstance()._extruder_trains[global_stack.getId()][stack.getMetaDataEntry("position")] = stack + ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId()) + else: + # Machine has no extruders, but it needs to be registered with the extruder manager. + ExtruderManager.getInstance().registerExtruder(None, global_stack.getId()) Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Notify everything/one that is to notify about changes. diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 3b33fa8661..4120c5b61e 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -128,7 +128,7 @@ UM.Dialog width: parent.width height: visible ? 25 : 0 text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?") - visible: false #manager.materialConflict + visible: false //manager.materialConflict Row { width: parent.width From 42be3c74727556367fe10f57a69f0c83ddc924ca Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 15:41:42 +0100 Subject: [PATCH 353/438] Conflict checker for machine now also checks if there is an actual difference CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 719ec1c8d7..04fc21b54a 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -15,6 +15,7 @@ from cura.Settings.ExtruderManager import ExtruderManager import zipfile import io +import configparser i18n_catalog = i18nCatalog("cura") @@ -62,8 +63,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_id = self._stripFileToId(container_stack_file) stacks = self._container_registry.findContainerStacks(id=container_id) if stacks: - machine_conflict = True - break + # Check if there are any changes at all in any of the container stacks. + id_list = self._getContainerIdListFromSerialized(archive.open(container_stack_file).read().decode("utf-8")) + for index, container_id in enumerate(id_list): + if stacks[0].getContainer(index).getId() != container_id: + machine_conflict = True + break material_conflict = False xml_material_profile = self._getXmlProfileClass() @@ -161,8 +166,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) else: - if self._resolve_strategies["material"] == "override": - pass + pass Logger.log("d", "Workspace loading is checking instance containers...") @@ -328,3 +332,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for type_name, container_type in self._container_registry.getContainerTypes(): if type_name == "XmlMaterialProfile": return container_type + + ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. + def _getContainerIdListFromSerialized(self, serialized): + parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False) + parser.read_string(serialized) + container_string = parser["general"].get("containers", "") + container_list = container_string.split(",") + return [container_id for container_id in container_list if container_id != ""] From 9f27e7861f6c6b5fc53d96ee37434d50b1ba042c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 16:04:03 +0100 Subject: [PATCH 354/438] Workspace reader now does a pre-check to see if it's a workspace in the first place CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 04fc21b54a..060617875d 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -99,6 +99,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if quality_changes[0] != instance_container: quality_changes_conflict = True break + try: + archive.open("Cura/preferences.cfg") + except KeyError: + # If there is no preferences file, it's not a workspace, so notify user of failure. + Logger.log("w", "File %s is not a valid workspace.", file_name) + return WorkspaceReader.PreReadResult.failed if machine_conflict or quality_changes_conflict or material_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort From a39d6824766384181827479760aa6720e8d0db26 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 16:07:18 +0100 Subject: [PATCH 355/438] Renamed Load workspace to open Workspace CURA-1263 --- resources/qml/Actions.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 2719d09cbc..9d910dc660 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -290,7 +290,7 @@ Item Action { id: loadWorkspaceAction - text: catalog.i18nc("@action:inmenu menubar:file","&Load Workspace..."); + text: catalog.i18nc("@action:inmenu menubar:file","&Open Workspace..."); } Action From 60577ea0d31554c7a8d16a62596b4092efe96635 Mon Sep 17 00:00:00 2001 From: Filip Goc Date: Wed, 16 Nov 2016 11:27:27 -0500 Subject: [PATCH 356/438] comment out ornamentals in starting gcode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit it worked before, but it’s better to be safe --- resources/definitions/jellybox.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/jellybox.def.json b/resources/definitions/jellybox.def.json index 432cf5a054..598203ef99 100644 --- a/resources/definitions/jellybox.def.json +++ b/resources/definitions/jellybox.def.json @@ -26,10 +26,10 @@ "machine_center_is_zero": { "default_value": false }, "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "---------------------------------------\n; ; ; Jellybox Start Script Begin ; ; ;\n_______________________________________\n; M92 E140 ;optionally adjust steps per mm for your filament\n\n; Print Settings Summary\n; (overwriting these values will NOT change your printer's behavior)\n; sliced for: {machine_name}\n; nozzle diameter: {machine_nozzle_size}\n; filament diameter: {material_diameter}\n; layer height: {layer_height}\n; 1st layer height: {layer_height_0}\n; line width: {line_width}\n; wall thickness: {wall_thickness}\n; infill density: {infill_sparse_density}\n; infill pattern: {infill_pattern}\n; print temperature: {material_print_temperature}\n; heated bed temperature: {material_bed_temperature}\n; regular fan speed: {cool_fan_speed_min}\n; max fan speed: {cool_fan_speed_max}\n; support? {support_enable}\n; spiralized? {magic_spiralize}\n\nM117 Preparing ;write Preparing\nM140 S{material_bed_temperature} ;set bed temperature and move on\nM104 S{material_print_temperature} ;set extruder temperature and move on\nM206 X10.0 Y0.0 ;set x homing offset for default bed leveling\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nM82 ;set extruder to absolute mode\nG28 ;home all axes\nM203 Z5 ;slow Z speed down for greater accuracy when probing\nG29 ;auto bed leveling procedure\nM203 Z7 ;pick up z speed again for printing\nM190 S{material_bed_temperature} ;wait for the bed to reach desired temperature\nM109 S{material_print_temperature} ;wait for the extruder to reach desired temperature\nG92 E0 ;reset the extruder position\nG1 F200 E5 ;extrude 5mm of feed stock\nG92 E0 ;reset the extruder position again\nM117 Print starting ;write Print starting\n---------------------------------------------\n; ; ; Jellybox Printer Start Script End ; ; ;\n_____________________________________________" + "default_value": ";---------------------------------------\n; ; ; Jellybox Start Script Begin ; ; ;\n;_______________________________________\n; M92 E140 ;optionally adjust steps per mm for your filament\n\n; Print Settings Summary\n; (overwriting these values will NOT change your printer's behavior)\n; sliced for: {machine_name}\n; nozzle diameter: {machine_nozzle_size}\n; filament diameter: {material_diameter}\n; layer height: {layer_height}\n; 1st layer height: {layer_height_0}\n; line width: {line_width}\n; wall thickness: {wall_thickness}\n; infill density: {infill_sparse_density}\n; infill pattern: {infill_pattern}\n; print temperature: {material_print_temperature}\n; heated bed temperature: {material_bed_temperature}\n; regular fan speed: {cool_fan_speed_min}\n; max fan speed: {cool_fan_speed_max}\n; support? {support_enable}\n; spiralized? {magic_spiralize}\n\nM117 Preparing ;write Preparing\nM140 S{material_bed_temperature} ;set bed temperature and move on\nM104 S{material_print_temperature} ;set extruder temperature and move on\nM206 X10.0 Y0.0 ;set x homing offset for default bed leveling\nG21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nM82 ;set extruder to absolute mode\nG28 ;home all axes\nM203 Z5 ;slow Z speed down for greater accuracy when probing\nG29 ;auto bed leveling procedure\nM203 Z7 ;pick up z speed again for printing\nM190 S{material_bed_temperature} ;wait for the bed to reach desired temperature\nM109 S{material_print_temperature} ;wait for the extruder to reach desired temperature\nG92 E0 ;reset the extruder position\nG1 F200 E5 ;extrude 5mm of feed stock\nG92 E0 ;reset the extruder position again\nM117 Print starting ;write Print starting\n;---------------------------------------------\n; ; ; Jellybox Printer Start Script End ; ; ;\n;_____________________________________________" }, "machine_end_gcode": { - "default_value": "\n---------------------------------\n;;; Jellybox End Script Begin ;;;\n_________________________________\nM117 Finishing Up ;write Finishing Up\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nG28 X ;home x, so the head is out of the way\nG1 Y100 ;move Y forward, so the print is more accessible\nM84 ;steppers off\n\nM117 Print finished ;write Print finished\n---------------------------------------\n;;; Jellybox End Script End ;;;\n_______________________________________" + "default_value": "\n;---------------------------------\n;;; Jellybox End Script Begin ;;;\n;_________________________________\nM117 Finishing Up ;write Finishing Up\n\nM104 S0 ;extruder heater off\nM140 S0 ;bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nG28 X ;home x, so the head is out of the way\nG1 Y100 ;move Y forward, so the print is more accessible\nM84 ;steppers off\n\nM117 Print finished ;write Print finished\n;---------------------------------------\n;;; Jellybox End Script End ;;;\n;_______________________________________" } } } From f5bf94c44d4d7e2b02ee5f63fee0f94d3a1a7656 Mon Sep 17 00:00:00 2001 From: Filip Goc Date: Wed, 16 Nov 2016 11:28:28 -0500 Subject: [PATCH 357/438] move platform mesh down 0.3mm apparently, it was just a bit too high, which made the bulild plate look foggy. --- resources/definitions/jellybox.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/jellybox.def.json b/resources/definitions/jellybox.def.json index 598203ef99..a6a1de0aa4 100644 --- a/resources/definitions/jellybox.def.json +++ b/resources/definitions/jellybox.def.json @@ -9,7 +9,7 @@ "manufacturer": "IMADE3D", "category": "Other", "platform": "jellybox_platform.stl", - "platform_offset": [ 0, 0, 0], + "platform_offset": [ 0, -0.3, 0], "file_formats": "text/x-gcode", "has_materials": true, "has_machine_materials": true From a52c62f6bb3b82b51cb5f54a66a12af38899e82a Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Wed, 16 Nov 2016 21:45:25 +0100 Subject: [PATCH 358/438] Adding debug message when type != 'printer' Just for the case we lose a device at this point. --- NetworkPrinterOutputDevicePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index bb1fade0bc..666c398382 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -194,6 +194,8 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): if info.properties.get(b"type", None) == b'printer': address = '.'.join(map(lambda n: str(n), info.address)) self.addPrinterSignal.emit(str(name), address, info.properties) + else: + Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % name) else: Logger.log("w", "Could not get information about %s" % name) From 0f6c7a94058473e8880e96b397d273f4d282cca5 Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Wed, 16 Nov 2016 21:50:11 +0100 Subject: [PATCH 359/438] Correcting warning .. also decoding the type of device and using it for the if clause, too. --- NetworkPrinterOutputDevicePlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index 666c398382..e69b700a6c 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -191,11 +191,12 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): info = zeroconf.get_service_info(service_type, name) if info: - if info.properties.get(b"type", None) == b'printer': + typeOfDevice = info.properties.get(b"type", None).decode("utf-8") + if typeOfDevice == "printer": address = '.'.join(map(lambda n: str(n), info.address)) self.addPrinterSignal.emit(str(name), address, info.properties) else: - Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % name) + Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." %typeOfDevice ) else: Logger.log("w", "Could not get information about %s" % name) From 3da5e9de4e34ad39d5ad6d970888012e7ccd7b39 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 16 Nov 2016 23:21:20 +0100 Subject: [PATCH 360/438] Add a "clear filter" button --- resources/qml/Settings/SettingView.qml | 125 ++++++++++++++++++------- 1 file changed, 91 insertions(+), 34 deletions(-) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 146049d814..70fd08b4bf 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -18,10 +18,24 @@ Item signal showTooltip(Item item, point location, string text); signal hideTooltip(); - TextField + Rectangle { - id: filter; - visible: !monitoringPrint + id: filterContainer + + border.width: UM.Theme.getSize("default_lining").width + border.color: + { + if(mouseArea.containsMouse || clearFilterButton.containsMouse) + { + return UM.Theme.getColor("setting_control_border_highlight"); + } + else + { + return UM.Theme.getColor("setting_control_border"); + } + } + + color: UM.Theme.getColor("setting_control") anchors { @@ -31,51 +45,94 @@ Item right: parent.right rightMargin: UM.Theme.getSize("default_margin").width } + height: UM.Theme.getSize("setting_control").height - placeholderText: catalog.i18nc("@label:textbox", "Filter...") - - style: TextFieldStyle + TextField { - textColor: UM.Theme.getColor("setting_control_text"); - font: UM.Theme.getFont("default"); - background: Rectangle - { - border.width: UM.Theme.getSize("default_lining").width - border.color: control.hovered ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") + id: filter; + visible: !monitoringPrint - color: UM.Theme.getColor("setting_control") + anchors.left: parent.left + anchors.right: clearFilterButton.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width + + placeholderText: catalog.i18nc("@label:textbox", "Filter...") + + style: TextFieldStyle + { + textColor: UM.Theme.getColor("setting_control_text"); + font: UM.Theme.getFont("default"); + background: Item {} + } + + property var expandedCategories + property bool lastFindingSettings: false + + onTextChanged: + { + definitionsModel.filter = {"label": "*" + text}; + findingSettings = (text.length > 0); + if(findingSettings != lastFindingSettings) + { + if(findingSettings) + { + expandedCategories = definitionsModel.expanded.slice(); + definitionsModel.expanded = ["*"]; + definitionsModel.showAncestors = true; + definitionsModel.showAll = true; + } + else + { + definitionsModel.expanded = expandedCategories; + definitionsModel.showAncestors = false; + definitionsModel.showAll = false; + } + lastFindingSettings = findingSettings; + } + } + + Keys.onEscapePressed: + { + filter.text = ""; } } - property var expandedCategories - property bool lastFindingSettings: false + MouseArea + { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: Qt.IBeamCursor + } - onTextChanged: { - definitionsModel.filter = {"label": "*" + text}; - findingSettings = (text.length > 0); - if(findingSettings != lastFindingSettings) + UM.SimpleButton + { + id: clearFilterButton + iconSource: UM.Theme.getIcon("cross1") + visible: findingSettings + + height: parent.height * 0.4 + width: visible ? height : 0 + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + + color: UM.Theme.getColor("setting_control_button") + hoverColor: UM.Theme.getColor("setting_control_button_hover") + + onClicked: { - if(findingSettings) - { - expandedCategories = definitionsModel.expanded.slice(); - definitionsModel.expanded = ["*"]; - definitionsModel.showAncestors = true; - definitionsModel.showAll = true; - } - else - { - definitionsModel.expanded = expandedCategories; - definitionsModel.showAncestors = false; - definitionsModel.showAll = false; - } - lastFindingSettings = findingSettings; + filter.text = ""; + filter.setActiveFocus(); } } } ScrollView { - anchors.top: filter.bottom; + anchors.top: filterContainer.bottom; anchors.bottom: parent.bottom; anchors.right: parent.right; anchors.left: parent.left; From 808417b152f1c1c69801bc78242f0a8b3b5ae404 Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Thu, 17 Nov 2016 10:28:35 +0100 Subject: [PATCH 361/438] Renaming typeOfDevice -> type_of_device --- NetworkPrinterOutputDevicePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index e69b700a6c..7a86002210 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -191,12 +191,12 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): info = zeroconf.get_service_info(service_type, name) if info: - typeOfDevice = info.properties.get(b"type", None).decode("utf-8") - if typeOfDevice == "printer": + type_of_device = info.properties.get(b"type", None).decode("utf-8") + if type_of_device == "printer": address = '.'.join(map(lambda n: str(n), info.address)) self.addPrinterSignal.emit(str(name), address, info.properties) else: - Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." %typeOfDevice ) + Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." %type_of_device ) else: Logger.log("w", "Could not get information about %s" % name) From 92a4fd723967038fe51a6a3468fc73dbffe90cd8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 17 Nov 2016 11:16:15 +0100 Subject: [PATCH 362/438] Materials are now also handled in conflict resolvement CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 28 ++++++++++++- plugins/3MFReader/WorkspaceDialog.qml | 2 +- .../XmlMaterialProfile/XmlMaterialProfile.py | 41 ++++++++++++++++--- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 060617875d..a6bc2e4a8c 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -158,6 +158,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._container_registry.addContainer(definition_container) Logger.log("d", "Workspace loading is checking materials...") + material_containers = [] # Get all the material files and check if they exist. If not, add them. xml_material_profile = self._getXmlProfileClass() if self._material_container_suffix is None: @@ -172,8 +173,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) else: - pass - + if not materials[0].isReadOnly(): # Only create new materials if they are not read only. + if self._resolve_strategies["material"] == "override": + materials[0].deserialize(archive.open(material_container_file).read().decode("utf-8")) + elif self._resolve_strategies["material"] == "new": + # Note that we *must* deserialize it with a new ID, as multiple containers will be + # auto created & added. + material_container = xml_material_profile(self.getNewId(container_id)) + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) + self._container_registry.addContainer(material_container) + material_containers.append(material_container) Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace @@ -311,6 +320,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + if self._resolve_strategies["material"] == "new": + for material in material_containers: + old_material = global_stack.findContainer({"type": "material"}) + if old_material.getId() in self._id_mapping: + material_index = global_stack.getContainerIndex(old_material) + global_stack.replaceContainer(material_index, material) + continue + + for stack in extruder_stacks: + old_material = stack.findContainer({"type": "material"}) + if old_material.getId() in self._id_mapping: + material_index = stack.getContainerIndex(old_material) + stack.replaceContainer(material_index, material) + continue + for stack in extruder_stacks: ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId()) else: diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 4120c5b61e..cdefd9a4b0 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -128,7 +128,7 @@ UM.Dialog width: parent.width height: visible ? 25 : 0 text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?") - visible: false //manager.materialConflict + visible: manager.materialConflict Row { width: parent.width diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 07acc5c37c..94f7368ab0 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -340,10 +340,22 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): mapping[key] = element first.append(element) + def clearData(self): + self._metadata = {} + self._name = "" + self._definition = None + self._instances = {} + self._read_only = False + self._dirty = False + self._path = "" + ## Overridden from InstanceContainer def deserialize(self, serialized): data = ET.fromstring(serialized) + # Reset previous metadata + self.clearData() # Ensure any previous data is gone. + self.addMetaDataEntry("type", "material") self.addMetaDataEntry("base_file", self.id) @@ -445,7 +457,16 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): definition = definitions[0] if machine_compatibility: - new_material = XmlMaterialProfile(self.id + "_" + machine_id) + new_material_id = self.id + "_" + machine_id + + # It could be that we are overwriting, so check if the ID already exists. + materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=new_material_id) + if materials: + new_material = materials[0] + new_material.clearData() + else: + new_material = XmlMaterialProfile(new_material_id) + new_material.setName(self.getName()) new_material.setMetaData(copy.deepcopy(self.getMetaData())) new_material.setDefinition(definition) @@ -459,9 +480,8 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): new_material.setProperty(key, "value", value, definition) new_material._dirty = False - - UM.Settings.ContainerRegistry.getInstance().addContainer(new_material) - + if not materials: + UM.Settings.ContainerRegistry.getInstance().addContainer(new_material) hotends = machine.iterfind("./um:hotend", self.__namespaces) for hotend in hotends: @@ -491,7 +511,15 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): else: Logger.log("d", "Unsupported material setting %s", key) - new_hotend_material = XmlMaterialProfile(self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")) + # It could be that we are overwriting, so check if the ID already exists. + new_hotend_id = self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_") + materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=new_hotend_id) + if materials: + new_hotend_material = materials[0] + new_hotend_material.clearData() + else: + new_hotend_material = XmlMaterialProfile(new_hotend_id) + new_hotend_material.setName(self.getName()) new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData())) new_hotend_material.setDefinition(definition) @@ -509,7 +537,8 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): new_hotend_material.setProperty(key, "value", value, definition) new_hotend_material._dirty = False - UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material) + if not materials: # It was not added yet, do so now. + UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material) def _addSettingElement(self, builder, instance): try: From 053ea6ad520df92f087632b35a851817123de21b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 17 Nov 2016 11:44:44 +0100 Subject: [PATCH 363/438] Bumped the used API version up by one CURA-1263 --- resources/qml/Cura.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 558a71c6d0..85be3342e9 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -7,7 +7,7 @@ import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 -import UM 1.2 as UM +import UM 1.3 as UM import Cura 1.0 as Cura import "Menus" From 77cb5d8a5aed9f3c339e918ffc3a35edf58a9822 Mon Sep 17 00:00:00 2001 From: Filip Goc Date: Thu, 17 Nov 2016 13:28:17 -0500 Subject: [PATCH 364/438] Tweak Machine Name JellyBOX -> IMADE3D JellyBOX --- resources/definitions/jellybox.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/jellybox.def.json b/resources/definitions/jellybox.def.json index a6a1de0aa4..fa3cb35cf7 100644 --- a/resources/definitions/jellybox.def.json +++ b/resources/definitions/jellybox.def.json @@ -16,7 +16,7 @@ }, "overrides": { - "machine_name": { "default_value": "JellyBOX" }, + "machine_name": { "default_value": "IMADE3D JellyBOX" }, "machine_width": { "default_value": 170 }, "machine_height": { "default_value": 145 }, "machine_depth": { "default_value": 160 }, From dfd512b637027438ebfb7304ee99c7eb44929706 Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Thu, 17 Nov 2016 21:12:28 +0100 Subject: [PATCH 365/438] UM3NetworkPrintingPlugin: Moving plugin files --- .../UM3NetworkPrintingPlugin/DiscoverUM3Action.py | 0 .../UM3NetworkPrintingPlugin/DiscoverUM3Action.qml | 0 .../UM3NetworkPrintingPlugin/NetworkPrinterOutputDevice.py | 0 .../UM3NetworkPrintingPlugin/NetworkPrinterOutputDevicePlugin.py | 0 .../UM3NetworkPrintingPlugin/UM3InfoComponents.qml | 0 __init__.py => plugins/UM3NetworkPrintingPlugin/__init__.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename DiscoverUM3Action.py => plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.py (100%) rename DiscoverUM3Action.qml => plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.qml (100%) rename NetworkPrinterOutputDevice.py => plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevice.py (100%) rename NetworkPrinterOutputDevicePlugin.py => plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevicePlugin.py (100%) rename UM3InfoComponents.qml => plugins/UM3NetworkPrintingPlugin/UM3InfoComponents.qml (100%) rename __init__.py => plugins/UM3NetworkPrintingPlugin/__init__.py (100%) diff --git a/DiscoverUM3Action.py b/plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.py similarity index 100% rename from DiscoverUM3Action.py rename to plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.py diff --git a/DiscoverUM3Action.qml b/plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.qml similarity index 100% rename from DiscoverUM3Action.qml rename to plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.qml diff --git a/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevice.py similarity index 100% rename from NetworkPrinterOutputDevice.py rename to plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevice.py diff --git a/NetworkPrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevicePlugin.py similarity index 100% rename from NetworkPrinterOutputDevicePlugin.py rename to plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevicePlugin.py diff --git a/UM3InfoComponents.qml b/plugins/UM3NetworkPrintingPlugin/UM3InfoComponents.qml similarity index 100% rename from UM3InfoComponents.qml rename to plugins/UM3NetworkPrintingPlugin/UM3InfoComponents.qml diff --git a/__init__.py b/plugins/UM3NetworkPrintingPlugin/__init__.py similarity index 100% rename from __init__.py rename to plugins/UM3NetworkPrintingPlugin/__init__.py From 8c4932f83778cd15ea252752129468f9f447d4e0 Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Thu, 17 Nov 2016 23:09:51 +0100 Subject: [PATCH 366/438] Cleanup --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 376bdd8547..9357e436ff 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD Cura ==== From 860ae66ce62df729e20031446265473b2c80720a Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Thu, 17 Nov 2016 23:10:42 +0100 Subject: [PATCH 367/438] Cleanup again --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6c4ee96d3a..3f7faebf5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -#Compiled and generated things. +# Compiled and generated things. build *.pyc __pycache__ @@ -31,6 +31,5 @@ debian* plugins/Doodle3D-cura-plugin plugins/GodMode plugins/PostProcessingPlugin -plugins/UM3NetworkPrinting plugins/X3GWriter From 9f468b8fcf0db933cc4fa7a96b99b60f6e00ad62 Mon Sep 17 00:00:00 2001 From: Thomas Karl Pietrowski Date: Fri, 18 Nov 2016 10:28:44 +0100 Subject: [PATCH 368/438] Correcting German translation Contributes to CURA-2862 --- resources/i18n/de/cura.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/i18n/de/cura.po b/resources/i18n/de/cura.po index b015b2e43b..985764f6ff 100644 --- a/resources/i18n/de/cura.po +++ b/resources/i18n/de/cura.po @@ -1913,7 +1913,7 @@ msgstr "&Beenden" #: /home/ruben/Projects/Cura/resources/qml/Actions.qml:97 msgctxt "@action:inmenu" msgid "Configure Cura..." -msgstr "Cura wird konfiguriert..." +msgstr "Cura konfigurieren..." #: /home/ruben/Projects/Cura/resources/qml/Actions.qml:104 msgctxt "@action:inmenu menubar:printer" From 657c93fff145432e61348583fde117f8a6156f2d Mon Sep 17 00:00:00 2001 From: Thomas-Karl Pietrowski Date: Sat, 19 Nov 2016 23:17:31 +0100 Subject: [PATCH 369/438] Adding debug message about correct authentication Might be useful when going through the logs and looking for authentication problems. --- NetworkPrinterOutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 356f61b172..56e674fabe 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -200,6 +200,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Logger.log("d", "Authentication was required. Setting up authenticator.") authenticator.setUser(self._authentication_id) authenticator.setPassword(self._authentication_key) + else: + Logger.log("d", "No authentication was required. The id is: %s", self._authentication_id) def getProperties(self): return self._properties From 7e4f1dce7bf030e4118cd29fefa1854c3e2a859b Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Mon, 21 Nov 2016 23:29:18 +0100 Subject: [PATCH 370/438] Add button for toggling the find settings textbox The toggle is only shown when advanced mode is selected. --- resources/qml/Sidebar.qml | 38 +++++++++++++++++++++++++- resources/themes/cura/icons/search.svg | 4 +++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 resources/themes/cura/icons/search.svg diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index 1c1eb5e5a0..a74b684213 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -214,7 +214,7 @@ Rectangle anchors.left: parent.left anchors.leftMargin: model.index * (settingsModeSelection.width / 2) anchors.verticalCenter: parent.verticalCenter - width: parent.width / 2 + width: 0.5 * parent.width - (index == 1 ? toggleFilterButton.width : 0) text: model.text exclusiveGroup: modeMenuGroup; checkable: true; @@ -256,6 +256,42 @@ Rectangle } } + Button + { + id: toggleFilterButton + + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.top: headerSeparator.bottom + anchors.topMargin: UM.Theme.getSize("default_margin").height + + height: settingsModeSelection.height + width: visible ? height : 0 + + visible: !monitoringPrint && base.currentModeIndex == 1 + opacity: visible ? 1 : 0 + + style: ButtonStyle + { + background: Rectangle + { + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("toggle_checked_border") + color: visible ? UM.Theme.getColor("toggle_checked") : UM.Theme.getColor("toggle_hovered") + Behavior on color { ColorAnimation { duration: 50; } } + } + label: UM.RecolorImage + { + anchors.verticalCenter: control.verticalCenter + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width / 2 + + source: UM.Theme.getIcon("search") + color: UM.Theme.getColor("toggle_checked_text") + } + } + } + Label { id: monitorLabel text: catalog.i18nc("@label","Printer Monitor"); diff --git a/resources/themes/cura/icons/search.svg b/resources/themes/cura/icons/search.svg new file mode 100644 index 0000000000..8272991300 --- /dev/null +++ b/resources/themes/cura/icons/search.svg @@ -0,0 +1,4 @@ + + + From c20a2a0a17fe1b58fef08f2e6c7f7748dd4d0249 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Tue, 22 Nov 2016 12:23:00 +0100 Subject: [PATCH 371/438] JSON language fix: clarify difference between Union Overlapping Volumes and Remove Mesh Intersection (CURA-2992) --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 19587e6562..4bd2d57973 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3763,7 +3763,7 @@ "meshfix_union_all": { "label": "Union Overlapping Volumes", - "description": "Ignore the internal geometry arising from overlapping volumes and print the volumes as one. This may cause internal cavities to disappear.", + "description": "Ignore the internal geometry arising from overlapping volumes within a mesh and print the volumes as one. This may cause unintended internal cavities to disappear.", "type": "bool", "default_value": true, "settable_per_mesh": true @@ -3795,7 +3795,7 @@ "carve_multiple_volumes": { "label": "Remove Mesh Intersection", - "description": "Remove areas where multiple objects are overlapping with each other. This may be used if merged dual material objects overlap with each other.", + "description": "Remove areas where multiple meshes are overlapping with each other. This may be used if merged dual material objects overlap with each other.", "type": "bool", "default_value": true, "value": "machine_extruder_count > 1", @@ -3806,7 +3806,7 @@ "alternate_carve_order": { "label": "Alternate Mesh Removal", - "description": "With every layer switch to which model the intersecting volumes will belong, so that the overlapping volumes become interwoven.", + "description": "Switch to which mesh intersecting volumes will belong with every layer, so that the overlapping meshes become interwoven. Turning this setting off will cause one of the meshes to obtain all of the volume in the overlap, while it is removed from the other meshes.", "type": "bool", "default_value": true, "enabled": "carve_multiple_volumes", From 174990ba9d5c0c4a808590f4d80e208dbba0884a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:13:30 +0100 Subject: [PATCH 372/438] Set the depends_on property for resolve to value CURA-2900 --- cura/CuraApplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index b89df31dd9..db17b50793 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -130,7 +130,7 @@ class CuraApplication(QtApplication): # For settings which are not settable_per_mesh and not settable_per_extruder: # A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders - SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None) + SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None, depends_on = "value") SettingDefinition.addSettingType("extruder", None, str, Validator) From 3a7370a09e69037f626fea235d5e79c623f50c20 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 12 Oct 2016 15:27:01 +0200 Subject: [PATCH 373/438] JSON feat: dual_pre_wipe as separate from prime_tower_wipe_enabled (CURA-2325) --- resources/definitions/fdmprinter.def.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 4bd2d57973..4989836a82 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3701,6 +3701,17 @@ "settable_per_mesh": false, "settable_per_extruder": false }, + "dual_pre_wipe": + { + "label": "Wipe Nozzle After Switch", + "description": "After switching nozzle, wipe the oozed material off of the nozzle on the first thing printed. This performs a safe slow wipe move at a place where the oozed material causes least harm to the surface quality of your print.", + "type": "bool", + "enabled": "resolveOrValue('prime_tower_enable')", + "default_value": true, + "resolve": "any(extruderValues('dual_pre_wipe'))", + "settable_per_mesh": false, + "settable_per_extruder": false + }, "multiple_mesh_overlap": { "label": "Dual Extrusion Overlap", From c1d600dfa7aa801ddee399bb6007339981eb0b2a Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 12 Oct 2016 15:27:43 +0200 Subject: [PATCH 374/438] JSON fix: rename Wipe Nozzle on Prime Tower to Wipe Inactive Nozzle on Prime Tower (CURA-2325) This way it's better distinguished from dual_pre_wipe --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 4989836a82..51b2837d41 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3692,7 +3692,7 @@ }, "prime_tower_wipe_enabled": { - "label": "Wipe Nozzle on Prime Tower", + "label": "Wipe Inactive Nozzle on Prime Tower", "description": "After printing the prime tower with one nozzle, wipe the oozed material from the other nozzle off on the prime tower.", "type": "bool", "enabled": "resolveOrValue('prime_tower_enable')", From 2c3f891885d7ba605c9ba2be127dc54f8cd17c27 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 12 Oct 2016 16:50:40 +0200 Subject: [PATCH 375/438] fix: make wipe settings settable per extruder (CURA-2325) --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 51b2837d41..7b43b523a3 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3699,7 +3699,7 @@ "default_value": true, "resolve": "any(extruderValues('prime_tower_wipe_enabled'))", "settable_per_mesh": false, - "settable_per_extruder": false + "settable_per_extruder": true }, "dual_pre_wipe": { @@ -3710,7 +3710,7 @@ "default_value": true, "resolve": "any(extruderValues('dual_pre_wipe'))", "settable_per_mesh": false, - "settable_per_extruder": false + "settable_per_extruder": true }, "multiple_mesh_overlap": { From 64aafcc8584c826a171170ab4e72dcc0032ef893 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Thu, 13 Oct 2016 09:48:14 +0200 Subject: [PATCH 376/438] JSON fix: make wiping settable per extruder (CURA-2325) --- resources/definitions/fdmprinter.def.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 7b43b523a3..2e638a1231 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3697,7 +3697,6 @@ "type": "bool", "enabled": "resolveOrValue('prime_tower_enable')", "default_value": true, - "resolve": "any(extruderValues('prime_tower_wipe_enabled'))", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -3708,7 +3707,6 @@ "type": "bool", "enabled": "resolveOrValue('prime_tower_enable')", "default_value": true, - "resolve": "any(extruderValues('dual_pre_wipe'))", "settable_per_mesh": false, "settable_per_extruder": true }, From c95f983b9b68d1838b13604ac8be65a52f62da90 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Thu, 13 Oct 2016 09:48:44 +0200 Subject: [PATCH 377/438] JSON feat: prime_tower_min_volume and prime_tower_wall_thickness (CURA-2325) --- resources/definitions/fdmprinter.def.json | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 2e638a1231..8ba538b2ea 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3646,6 +3646,39 @@ "settable_per_mesh": false, "settable_per_extruder": false }, + "prime_tower_min_volume": + { + "label": "Prime Tower Minimum Volume", + "description": "The minimum volume each layer of the prime tower in order to purge enough material.", + "unit": "mm³", + "type": "float", + "default_value": 4.544, + "minimum_value": "0", + "maximum_value_warning": "resolveOrValue('prime_tower_size') * resolveOrValue('prime_tower_size') * resolveOrValue('layer_height')", + "enabled": "resolveOrValue('prime_tower_enable')", + "resolve": "max(extruderValues('prime_tower_min_volume'))", + "settable_per_mesh": false, + "settable_per_extruder": false, + "children": + { + "prime_tower_wall_thickness": + { + "label": "Prime Tower Wall Thickness", + "description": "The thickness of the outside walls in the horizontal direction. This value divided by the wall line width defines the number of walls.", + "unit": "mm", + "type": "float", + "default_value": 0.8, + "value": "max(2 * min(extruderValues('prime_tower_line_width')), 0.5 * (resolveOrValue('prime_tower_size') - math.sqrt(resolveOrValue('prime_tower_size') * resolveOrValue('prime_tower_size') - max(extruderValues('prime_tower_min_volume')) / resolveOrValue('layer_height'))))", + "resolve": "max(extruderValues('prime_tower_wall_thickness'))", + "minimum_value": "0.001", + "minimum_value_warning": "2 * min(extruderValues('prime_tower_line_width'))", + "maximum_value_warning": "resolveOrValue('prime_tower_size') / 2", + "enabled": "resolveOrValue('prime_tower_enable')", + "settable_per_mesh": false, + "settable_per_extruder": false + } + } + }, "prime_tower_position_x": { "label": "Prime Tower X Position", From 8016cb648bc7c98d9097df018f2a9819cb352236 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Thu, 13 Oct 2016 20:09:28 +0200 Subject: [PATCH 378/438] JSOn fix: better language hollow wipe tower settings (CURA-2325) --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 8ba538b2ea..4d6e78d03e 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3649,7 +3649,7 @@ "prime_tower_min_volume": { "label": "Prime Tower Minimum Volume", - "description": "The minimum volume each layer of the prime tower in order to purge enough material.", + "description": "The minimum volume for each layer of the prime tower in order to purge enough material.", "unit": "mm³", "type": "float", "default_value": 4.544, @@ -3663,8 +3663,8 @@ { "prime_tower_wall_thickness": { - "label": "Prime Tower Wall Thickness", - "description": "The thickness of the outside walls in the horizontal direction. This value divided by the wall line width defines the number of walls.", + "label": "Prime Tower Thickness", + "description": "The thickness of the hollow prime tower. A thickness larger than half the Prime Tower Minimum Volume will result in a dense prime tower.", "unit": "mm", "type": "float", "default_value": 0.8, From 5e2f055cfe1bb2c10c27ee03ce22c339f5425d14 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Thu, 13 Oct 2016 20:11:02 +0200 Subject: [PATCH 379/438] JSON fix: better defaults hollow prime tower settings (CURA-2325) --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 4d6e78d03e..e35dcd9a72 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3652,7 +3652,7 @@ "description": "The minimum volume for each layer of the prime tower in order to purge enough material.", "unit": "mm³", "type": "float", - "default_value": 4.544, + "default_value": 10, "minimum_value": "0", "maximum_value_warning": "resolveOrValue('prime_tower_size') * resolveOrValue('prime_tower_size') * resolveOrValue('layer_height')", "enabled": "resolveOrValue('prime_tower_enable')", @@ -3667,7 +3667,7 @@ "description": "The thickness of the hollow prime tower. A thickness larger than half the Prime Tower Minimum Volume will result in a dense prime tower.", "unit": "mm", "type": "float", - "default_value": 0.8, + "default_value": 2, "value": "max(2 * min(extruderValues('prime_tower_line_width')), 0.5 * (resolveOrValue('prime_tower_size') - math.sqrt(resolveOrValue('prime_tower_size') * resolveOrValue('prime_tower_size') - max(extruderValues('prime_tower_min_volume')) / resolveOrValue('layer_height'))))", "resolve": "max(extruderValues('prime_tower_wall_thickness'))", "minimum_value": "0.001", From 61780b49800f72761b614cf9943eb349cd4a2ab5 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Fri, 14 Oct 2016 13:02:04 +0200 Subject: [PATCH 380/438] make prime_tower_min_volume settable per extruder (CURA-2325) --- resources/definitions/fdmprinter.def.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index e35dcd9a72..c5174cbccc 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3656,9 +3656,8 @@ "minimum_value": "0", "maximum_value_warning": "resolveOrValue('prime_tower_size') * resolveOrValue('prime_tower_size') * resolveOrValue('layer_height')", "enabled": "resolveOrValue('prime_tower_enable')", - "resolve": "max(extruderValues('prime_tower_min_volume'))", "settable_per_mesh": false, - "settable_per_extruder": false, + "settable_per_extruder": true, "children": { "prime_tower_wall_thickness": From 1e343c26ebb6f0c07b7f0b5b8a48f12000f8dcd8 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 2 Nov 2016 10:23:42 +0100 Subject: [PATCH 381/438] JSON fix: better description dual_pre_wipe (CURA-2325) --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index c5174cbccc..8547aa4df6 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3735,7 +3735,7 @@ "dual_pre_wipe": { "label": "Wipe Nozzle After Switch", - "description": "After switching nozzle, wipe the oozed material off of the nozzle on the first thing printed. This performs a safe slow wipe move at a place where the oozed material causes least harm to the surface quality of your print.", + "description": "After switching extruder, wipe the oozed material off of the nozzle on the first thing printed. This performs a safe slow wipe move at a place where the oozed material causes least harm to the surface quality of your print.", "type": "bool", "enabled": "resolveOrValue('prime_tower_enable')", "default_value": true, From e0b4246336fa4f5c6055fcda3251f75501c48b31 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Wed, 2 Nov 2016 10:29:16 +0100 Subject: [PATCH 382/438] JSON fix: use exponent function ** 2 rather than x * x (CURA-2325) --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 8547aa4df6..364608767c 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3654,7 +3654,7 @@ "type": "float", "default_value": 10, "minimum_value": "0", - "maximum_value_warning": "resolveOrValue('prime_tower_size') * resolveOrValue('prime_tower_size') * resolveOrValue('layer_height')", + "maximum_value_warning": "resolveOrValue('prime_tower_size') ** 2 * resolveOrValue('layer_height')", "enabled": "resolveOrValue('prime_tower_enable')", "settable_per_mesh": false, "settable_per_extruder": true, @@ -3667,7 +3667,7 @@ "unit": "mm", "type": "float", "default_value": 2, - "value": "max(2 * min(extruderValues('prime_tower_line_width')), 0.5 * (resolveOrValue('prime_tower_size') - math.sqrt(resolveOrValue('prime_tower_size') * resolveOrValue('prime_tower_size') - max(extruderValues('prime_tower_min_volume')) / resolveOrValue('layer_height'))))", + "value": "max(2 * min(extruderValues('prime_tower_line_width')), 0.5 * (resolveOrValue('prime_tower_size') - math.sqrt(resolveOrValue('prime_tower_size') ** 2 - max(extruderValues('prime_tower_min_volume')) / resolveOrValue('layer_height'))))", "resolve": "max(extruderValues('prime_tower_wall_thickness'))", "minimum_value": "0.001", "minimum_value_warning": "2 * min(extruderValues('prime_tower_line_width'))", From 1bc0fdebc9d971b0a1adf91a8779bbc4109f8f50 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Thu, 3 Nov 2016 20:06:28 +0100 Subject: [PATCH 383/438] JSON fix: disable post-wipe on prime tower for UM3 (CURA-2325) it has nozzle lifting so it's impossible! --- resources/definitions/ultimaker3.def.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 1356efff91..243cf58a8b 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -70,6 +70,7 @@ "machine_end_gcode": { "default_value": "" }, "prime_tower_position_x": { "default_value": 175 }, "prime_tower_position_y": { "default_value": 179 }, + "prime_tower_wipe_enabled": { "default_value": false }, "acceleration_enabled": { "value": "True" }, "acceleration_layer_0": { "value": "acceleration_topbottom" }, From 42caf57993e15fe09fa4924fb527125e2b4ce2ee Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:31:58 +0100 Subject: [PATCH 384/438] Added "material" to default resolve strategies CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index a6bc2e4a8c..adde4a79e4 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -56,7 +56,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] - self._resolve_strategies = {"machine": None, "quality_changes": None} + self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None} machine_conflict = False quality_changes_conflict = False for container_stack_file in container_stack_files: From cba31d95ec67b919fefa99fba0b43b6fa7764f2c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:33:10 +0100 Subject: [PATCH 385/438] Workspace reader now loads from prefered suffix CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index adde4a79e4..5a818a2407 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -28,10 +28,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None self._container_registry = ContainerRegistry.getInstance() - self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] + self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it - self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] - self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] + self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix + self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix self._resolve_strategies = {} @@ -73,7 +73,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_conflict = False xml_material_profile = self._getXmlProfileClass() if self._material_container_suffix is None: - self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] + self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).preferredSuffix if xml_material_profile: material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)] for material_container_file in material_container_files: From e3eb75ab6eef45a7bdd70d4a450c54fec249d6ff Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:37:11 +0100 Subject: [PATCH 386/438] We now get material container by mimetype CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 5a818a2407..265e4cc074 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -7,6 +7,7 @@ from UM.Settings.ContainerStack import ContainerStack from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry +from UM.MimeTypeDatabase import MimeTypeDatabase from UM.Preferences import Preferences from .WorkspaceDialog import WorkspaceDialog @@ -359,9 +360,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return file.replace("Cura/", "").split(".")[0] def _getXmlProfileClass(self): - for type_name, container_type in self._container_registry.getContainerTypes(): - if type_name == "XmlMaterialProfile": - return container_type + return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. def _getContainerIdListFromSerialized(self, serialized): From 8a67f44cf0967936405d3b5ddc9f42b1ba567fdd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:38:06 +0100 Subject: [PATCH 387/438] We now also write with the preferedSuffix CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index b6c9d884af..cafc18858f 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -59,7 +59,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()): return # Empty file, do nothing. - file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).suffixes[0] + file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).preferredSuffix # Some containers have a base file, which should then be the file to use. if "base_file" in container.getMetaData(): From 933ed5177fec4a900e2813444ab40bf1a26099b5 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 22 Nov 2016 13:44:41 +0100 Subject: [PATCH 388/438] Toggle visibility of filter field when pressing the "search" icon --- resources/qml/Settings/SettingView.qml | 22 +++++++++++++++++++--- resources/qml/Sidebar.qml | 10 ++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 70fd08b4bf..c3da68bf79 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -18,9 +18,23 @@ Item signal showTooltip(Item item, point location, string text); signal hideTooltip(); + function toggleFilterField() + { + filterContainer.visible = !filterContainer.visible + if(filterContainer.visible) + { + filter.forceActiveFocus(); + } + else + { + filter.text = ""; + } + } + Rectangle { id: filterContainer + visible: false border.width: UM.Theme.getSize("default_lining").width border.color: @@ -45,7 +59,8 @@ Item right: parent.right rightMargin: UM.Theme.getSize("default_margin").width } - height: UM.Theme.getSize("setting_control").height + height: visible ? UM.Theme.getSize("setting_control").height : 0 + Behavior on height { NumberAnimation { duration: 100 } } TextField { @@ -125,7 +140,7 @@ Item onClicked: { filter.text = ""; - filter.setActiveFocus(); + filter.forceActiveFocus(); } } } @@ -136,7 +151,8 @@ Item anchors.bottom: parent.bottom; anchors.right: parent.right; anchors.left: parent.left; - anchors.topMargin: UM.Theme.getSize("default_margin").width + anchors.topMargin: filterContainer.visible ? UM.Theme.getSize("default_margin").width : 0 + Behavior on anchors.topMargin { NumberAnimation { duration: 100 } } style: UM.Theme.styles.scrollview; flickableItem.flickableDirection: Flickable.VerticalFlick; diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index a74b684213..c2e6737ad4 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -214,7 +214,7 @@ Rectangle anchors.left: parent.left anchors.leftMargin: model.index * (settingsModeSelection.width / 2) anchors.verticalCenter: parent.verticalCenter - width: 0.5 * parent.width - (index == 1 ? toggleFilterButton.width : 0) + width: 0.5 * parent.width - (modesListModel.get(index).showFilterButton ? toggleFilterButton.width : 0) text: model.text exclusiveGroup: modeMenuGroup; checkable: true; @@ -268,9 +268,11 @@ Rectangle height: settingsModeSelection.height width: visible ? height : 0 - visible: !monitoringPrint && base.currentModeIndex == 1 + visible: !monitoringPrint && modesListModel.get(base.currentModeIndex).showFilterButton opacity: visible ? 1 : 0 + onClicked: sidebarContents.currentItem.toggleFilterField() + style: ButtonStyle { background: Rectangle @@ -415,8 +417,8 @@ Rectangle Component.onCompleted: { - modesListModel.append({ text: catalog.i18nc("@title:tab", "Recommended"), item: sidebarSimple }) - modesListModel.append({ text: catalog.i18nc("@title:tab", "Custom"), item: sidebarAdvanced }) + modesListModel.append({ text: catalog.i18nc("@title:tab", "Recommended"), item: sidebarSimple, showFilterButton: false }) + modesListModel.append({ text: catalog.i18nc("@title:tab", "Custom"), item: sidebarAdvanced, showFilterButton: true }) sidebarContents.push({ "item": modesListModel.get(base.currentModeIndex).item, "immediate": true }); } From 60d2d0d0920df115205d9ee5bec2b83ed420c5c6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 14:15:12 +0100 Subject: [PATCH 389/438] Workspace reader is now a lot more transactional; Instead of adding the instance containers on the go, we add them right before serializing the stack. This enables us to remove them if the stack serialization goes wrong CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 98 +++++++++++++-------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 265e4cc074..7340df3bf0 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -144,6 +144,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._id_mapping = {} + # We don't add containers right away, but wait right until right before the stack serialization. + # We do this so that if something goes wrong, it's easier to clean up. + containers_to_add = [] + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. @@ -172,7 +176,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not materials: material_container = xml_material_profile(container_id) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) - self._container_registry.addContainer(material_container) + containers_to_add.append(material_container) else: if not materials[0].isReadOnly(): # Only create new materials if they are not read only. if self._resolve_strategies["material"] == "override": @@ -182,7 +186,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # auto created & added. material_container = xml_material_profile(self.getNewId(container_id)) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) - self._container_registry.addContainer(material_container) + containers_to_add.append(material_container) material_containers.append(material_container) Logger.log("d", "Workspace loading is checking instance containers...") @@ -201,7 +205,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check if quality changes already exists. user_containers = self._container_registry.findInstanceContainers(id=container_id) if not user_containers: - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) else: if self._resolve_strategies["machine"] == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) @@ -213,7 +217,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id)) - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: @@ -221,13 +225,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry("machine", self.getNewId(machine_id)) - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers(id = container_id) if not quality_changes: - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) else: if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) @@ -238,49 +242,67 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: continue + # Add all the containers right before we try to add / serialize the stack + for container in containers_to_add: + self._container_registry.addContainer(container) + # Get the stack(s) saved in the workspace. Logger.log("d", "Workspace loading is checking stacks containers...") container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] global_stack = None extruder_stacks = [] - for container_stack_file in container_stack_files: - container_id = self._stripFileToId(container_stack_file) + container_stacks_added = [] + try: + for container_stack_file in container_stack_files: + container_id = self._stripFileToId(container_stack_file) - # Check if a stack by this ID already exists; - container_stacks = self._container_registry.findContainerStacks(id=container_id) - if container_stacks: - stack = container_stacks[0] - if self._resolve_strategies["machine"] == "override": - container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) - elif self._resolve_strategies["machine"] == "new": - new_id = self.getNewId(container_id) - stack = ContainerStack(new_id) - stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Check if a stack by this ID already exists; + container_stacks = self._container_registry.findContainerStacks(id=container_id) + if container_stacks: + stack = container_stacks[0] + if self._resolve_strategies["machine"] == "override": + container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) + elif self._resolve_strategies["machine"] == "new": + new_id = self.getNewId(container_id) + stack = ContainerStack(new_id) + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - # Ensure a unique ID and name - stack._id = new_id + # Ensure a unique ID and name + stack._id = new_id - # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the - # bound machine also needs to change. - if stack.getMetaDataEntry("machine", None): - stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) + # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the + # bound machine also needs to change. + if stack.getMetaDataEntry("machine", None): + stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) - if stack.getMetaDataEntry("type") != "extruder_train": - # Only machines need a new name, stacks may be non-unique - stack.setName(self._container_registry.uniqueName(stack.getName())) - self._container_registry.addContainer(stack) + if stack.getMetaDataEntry("type") != "extruder_train": + # Only machines need a new name, stacks may be non-unique + stack.setName(self._container_registry.uniqueName(stack.getName())) + container_stacks_added.append(stack) + self._container_registry.addContainer(stack) + else: + Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) else: - Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) - else: - stack = ContainerStack(container_id) - # Deserialize stack by converting read data from bytes to string - stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - self._container_registry.addContainer(stack) + stack = ContainerStack(container_id) + # Deserialize stack by converting read data from bytes to string + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + container_stacks_added.append(stack) + self._container_registry.addContainer(stack) - if stack.getMetaDataEntry("type") == "extruder_train": - extruder_stacks.append(stack) - else: - global_stack = stack + if stack.getMetaDataEntry("type") == "extruder_train": + extruder_stacks.append(stack) + else: + global_stack = stack + except: + Logger.log("W", "We failed to serialize the stack. Trying to clean up.") + # Something went really wrong. Try to remove any data that we added. + for container in containers_to_add: + self._container_registry.getInstance().removeContainer(container.getId()) + + for container in container_stacks_added: + self._container_registry.getInstance().removeContainer(container.getId()) + + return None if self._resolve_strategies["machine"] == "new": # A new machine was made, but it was serialized with the wrong user container. Fix that now. From fa174763cf43050c61fd82b0fb18e9bcca04a656 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 14:57:44 +0100 Subject: [PATCH 390/438] The 3mf workspace reader no longer locks application if it is accedently called from main CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 3 +++ plugins/3MFReader/WorkspaceDialog.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 7340df3bf0..79f2399cf7 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -113,7 +113,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setQualityChangesConflict(quality_changes_conflict) self._dialog.setMaterialConflict(material_conflict) self._dialog.show() + + # Block until the dialog is closed. self._dialog.waitForClose() + if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 96a22e4cd7..bf9dce8264 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -1,4 +1,4 @@ -from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty +from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty, QCoreApplication from PyQt5.QtQml import QQmlComponent, QQmlContext from UM.PluginRegistry import PluginRegistry from UM.Application import Application @@ -6,6 +6,7 @@ from UM.Logger import Logger import os import threading +import time class WorkspaceDialog(QObject): showDialogSignal = pyqtSignal() @@ -82,7 +83,8 @@ class WorkspaceDialog(QObject): def show(self): # Emit signal so the right thread actually shows the view. - self._lock.acquire() + if threading.current_thread() != threading.main_thread(): + self._lock.acquire() # Reset the result self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy, @@ -116,8 +118,14 @@ class WorkspaceDialog(QObject): ## Block thread until the dialog is closed. def waitForClose(self): if self._visible: - self._lock.acquire() - self._lock.release() + if threading.current_thread() != threading.main_thread(): + self._lock.acquire() + self._lock.release() + else: + # If this is not run from a separate thread, we need to ensure that the events are still processed. + while self._visible: + time.sleep(1 / 50) + QCoreApplication.processEvents() # Ensure that the GUI does not freeze. def __show(self): if self._view is None: From 75e067cab7153bdcea4d540481ffef1cd7ff3b51 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 15:24:01 +0100 Subject: [PATCH 391/438] No longer select last_entry that isn't in the list CURA-2860 --- cura/Settings/SettingInheritanceManager.py | 4 ++++ resources/qml/Settings/SettingItem.qml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 4d1e60a739..6e8c13640c 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -168,12 +168,16 @@ class SettingInheritanceManager(QObject): if value is not None: # If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value has_setting_function = isinstance(value, UM.Settings.SettingFunction) and len(value.getUsedSettingKeys()) > 0 + if key == "prime_tower_size": + print(container.getId()) if has_setting_function is False: has_non_function_value = True continue if has_setting_function: break # There is a setting function somewhere, stop looking deeper. + if key == "prime_tower_size": + print("YAY", has_setting_function, has_non_function_value) return has_setting_function and has_non_function_value def _update(self): diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml index 2aa15e9244..5e1e4b21dc 100644 --- a/resources/qml/Settings/SettingItem.qml +++ b/resources/qml/Settings/SettingItem.qml @@ -227,7 +227,7 @@ Item { focus = true; // Get the most shallow function value (eg not a number) that we can find. - var last_entry = propertyProvider.stackLevels[propertyProvider.stackLevels.length] + var last_entry = propertyProvider.stackLevels[propertyProvider.stackLevels.length - 1] for (var i = 1; i < base.stackLevels.length; i++) { var has_setting_function = typeof(propertyProvider.getPropertyValue("value", base.stackLevels[i])) == "object"; From e17eda9a3382d0ebcb35477f1324a6241405f881 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 15:25:54 +0100 Subject: [PATCH 392/438] Removed debug prints CURA-2860 --- cura/Settings/SettingInheritanceManager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 6e8c13640c..4d1e60a739 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -168,16 +168,12 @@ class SettingInheritanceManager(QObject): if value is not None: # If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value has_setting_function = isinstance(value, UM.Settings.SettingFunction) and len(value.getUsedSettingKeys()) > 0 - if key == "prime_tower_size": - print(container.getId()) if has_setting_function is False: has_non_function_value = True continue if has_setting_function: break # There is a setting function somewhere, stop looking deeper. - if key == "prime_tower_size": - print("YAY", has_setting_function, has_non_function_value) return has_setting_function and has_non_function_value def _update(self): From 124e2b47eac0b5b3587dfbb1ca66d59f6f94c4d8 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 23 Nov 2016 11:56:27 +0100 Subject: [PATCH 393/438] Fix display issue layer view reset positions, center point in select all. CURA-2925 CURA-3012 --- cura/CuraApplication.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 4e33cc2e1e..ff70a2e25c 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -734,6 +734,8 @@ class CuraApplication(QtApplication): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data Selection.add(node) ## Delete all nodes containing mesh data in the scene. @@ -773,6 +775,8 @@ class CuraApplication(QtApplication): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data nodes.append(node) if nodes: @@ -799,6 +803,8 @@ class CuraApplication(QtApplication): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data nodes.append(node) if nodes: From 716ffe94ffcf550fe479587b4c42baae3dec2296 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 23 Nov 2016 12:41:53 +0100 Subject: [PATCH 394/438] Don't show inheritance icon for resovled values CURA-2860 --- resources/qml/Settings/SettingItem.qml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml index 5e1e4b21dc..a03e2c599f 100644 --- a/resources/qml/Settings/SettingItem.qml +++ b/resources/qml/Settings/SettingItem.qml @@ -209,14 +209,26 @@ Item { // But this will cause the binding to be re-evaluated when the enabled property changes. return false; } + + // There are no settings with any warning. if(Cura.SettingInheritanceManager.settingsWithInheritanceWarning.length == 0) { return false; } + + // This setting has a resolve value, so an inheritance warning doesn't do anything. + if(resolve != "None") + { + return false + } + + // If the setting does not have a limit_to_extruder property (or is -1), use the active stack. if(globalPropertyProvider.properties.limit_to_extruder == null || globalPropertyProvider.properties.limit_to_extruder == -1) { return Cura.SettingInheritanceManager.settingsWithInheritanceWarning.indexOf(definition.key) >= 0; } + + // Setting does have a limit_to_extruder property, so use that one instead. return Cura.SettingInheritanceManager.getOverridesForExtruder(definition.key, globalPropertyProvider.properties.limit_to_extruder).indexOf(definition.key) >= 0; } From 54df4c17e107c9098ff933ce137a645fe76c7f26 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 23 Nov 2016 12:44:53 +0100 Subject: [PATCH 395/438] Add Dutch translation for 'Information' title This is to make it easier to test whether this title can be translated, both for me and for our QA team. Contributes to issue CURA-2808. --- resources/i18n/nl/cura.po | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/i18n/nl/cura.po b/resources/i18n/nl/cura.po index 2c8c83df7a..64e140474c 100644 --- a/resources/i18n/nl/cura.po +++ b/resources/i18n/nl/cura.po @@ -2629,3 +2629,8 @@ msgstr "De configuratie van de printer in Cura laden" msgctxt "@action:button" msgid "Activate Configuration" msgstr "Configuratie Activeren" + +#: /home/ruben/Projects/Cura/resources/qml/Preferences/MaterialView.qml:25 +msgctxt "@title" +msgid "Information" +msgstr "Informatie" From c1dda7505ce0c45d0db80a851ed520d6a12c0773 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 23 Nov 2016 12:47:54 +0100 Subject: [PATCH 396/438] Inheritance manager now also checks for inheritance if enabled property changed CURA-2860 --- cura/Settings/SettingInheritanceManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 4d1e60a739..7eecdf9591 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -85,7 +85,7 @@ class SettingInheritanceManager(QObject): self._update() # Ensure that the settings_with_inheritance_warning list is populated. def _onPropertyChanged(self, key, property_name): - if property_name == "value" and self._global_container_stack: + if (property_name == "value" or property_name == "enabled") and self._global_container_stack: definitions = self._global_container_stack.getBottom().findDefinitions(key = key) if not definitions: return From e77d7f1f204e344101061c450868b997c4ea0246 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 23 Nov 2016 13:22:02 +0100 Subject: [PATCH 397/438] PrinterOutput now keeps track of camera state CURA-2411 --- cura/PrinterOutputDevice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 6eae259e1e..7eb37037b9 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -49,6 +49,8 @@ class PrinterOutputDevice(QObject, OutputDevice): self._printer_state = "" self._printer_type = "unknown" + self._camera_active = False + def requestWrite(self, nodes, file_name = None, filter_by_machine = False): raise NotImplementedError("requestWrite needs to be implemented") @@ -136,6 +138,7 @@ class PrinterOutputDevice(QObject, OutputDevice): @pyqtSlot() def startCamera(self): + self._camera_active = True self._startCamera() def _startCamera(self): @@ -143,6 +146,7 @@ class PrinterOutputDevice(QObject, OutputDevice): @pyqtSlot() def stopCamera(self): + self._camera_active = False self._stopCamera() def _stopCamera(self): From f7e99144012f355e6492d6930bf02648d56b3e96 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 23 Nov 2016 13:23:01 +0100 Subject: [PATCH 398/438] Camera stream is now restored if the printer got a timeout CURA-2411 --- NetworkPrinterOutputDevice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 56e674fabe..931d4712dc 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -814,6 +814,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. Logger.log("d", "We got a response (%s) from the server after %0.1f of silence. Going back to previous state %s", reply.url().toString(), time() - self._last_response_time, self._connection_state_before_timeout) + + # Camera was active before timeout. Start it again + if self._camera_active: + self._startCamera() + self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None From c3320bb1a46681025307379949ca8297c749a055 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 23 Nov 2016 13:33:12 +0100 Subject: [PATCH 399/438] Change settings to settings/overrides in selected messages. CURA-2898 --- cura/Settings/MachineManager.py | 23 +++++++++++++--------- resources/qml/Actions.qml | 4 ++-- resources/qml/Preferences/ProfilesPage.qml | 4 ++-- resources/qml/SidebarHeader.qml | 2 +- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index fce82212cd..f0aa79541e 100644 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -871,7 +871,7 @@ class MachineManager(QObject): def _askUserToKeepOrClearCurrentSettings(self): # Ask the user if the user profile should be cleared or not (discarding the current settings) # In Simple Mode we assume the user always wants to keep the (limited) current settings - details_text = catalog.i18nc("@label", "You made changes to the following setting(s):") + details_text = catalog.i18nc("@label", "You made changes to the following setting(s)/override(s):") # user changes in global stack details_list = [setting.definition.label for setting in self._global_container_stack.getTop().findInstances(**{})] @@ -886,14 +886,19 @@ class MachineManager(QObject): # Format to output string details = "\n ".join([details_text, ] + details_list) - Application.getInstance().messageBox(catalog.i18nc("@window:title", "Switched profiles"), - catalog.i18nc("@label", - "Do you want to transfer your changed settings to this profile?"), - catalog.i18nc("@label", - "If you transfer your settings they will override settings in the profile."), - details, - buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, - callback=self._keepUserSettingsDialogCallback) + num_changed_settings = len(details_list) + Application.getInstance().messageBox( + catalog.i18nc("@window:title", "Switched profiles"), + catalog.i18nc( + "@label", + "Do you want to transfer your %d changed setting(s)/override(s) to this profile?") % num_changed_settings, + catalog.i18nc( + "@label", + "If you transfer your settings they will override settings in the profile."), + details, + buttons=QMessageBox.Yes + QMessageBox.No, + icon=QMessageBox.Question, + callback=self._keepUserSettingsDialogCallback) def _keepUserSettingsDialogCallback(self, button): if button == QMessageBox.Yes: diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 043552d768..23fb452605 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -122,7 +122,7 @@ Item { id: updateProfileAction; enabled: !Cura.MachineManager.stacksHaveErrors && Cura.MachineManager.hasUserSettings && !Cura.MachineManager.isReadOnly(Cura.MachineManager.activeQualityId) - text: catalog.i18nc("@action:inmenu menubar:profile","&Update profile with current settings"); + text: catalog.i18nc("@action:inmenu menubar:profile","&Update profile with current settings/overrides"); onTriggered: Cura.ContainerManager.updateQualityChanges(); } @@ -142,7 +142,7 @@ Item { id: addProfileAction; enabled: !Cura.MachineManager.stacksHaveErrors && Cura.MachineManager.hasUserSettings - text: catalog.i18nc("@action:inmenu menubar:profile","&Create profile from current settings..."); + text: catalog.i18nc("@action:inmenu menubar:profile","&Create profile from current settings/overrides..."); } Action diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml index b1f06af3a9..521145f872 100644 --- a/resources/qml/Preferences/ProfilesPage.qml +++ b/resources/qml/Preferences/ProfilesPage.qml @@ -162,7 +162,7 @@ UM.ManagementPage Button { text: { - return catalog.i18nc("@action:button", "Update profile with current settings"); + return catalog.i18nc("@action:button", "Update profile with current settings/overrides"); } enabled: Cura.MachineManager.hasUserSettings && !Cura.MachineManager.isReadOnly(Cura.MachineManager.activeQualityId) onClicked: Cura.ContainerManager.updateQualityChanges() @@ -187,7 +187,7 @@ UM.ManagementPage Label { id: defaultsMessage visible: false - text: catalog.i18nc("@action:label", "This profile uses the defaults specified by the printer, so it has no settings in the list below.") + text: catalog.i18nc("@action:label", "This profile uses the defaults specified by the printer, so it has no settings/overrides in the list below.") wrapMode: Text.WordWrap width: parent.width } diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 700384c394..e894392b06 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -329,7 +329,7 @@ Column } onEntered: { - var content = catalog.i18nc("@tooltip","Some setting values are different from the values stored in the profile.\n\nClick to open the profile manager.") + var content = catalog.i18nc("@tooltip","Some setting/override values are different from the values stored in the profile.\n\nClick to open the profile manager.") base.showTooltip(globalProfileRow, Qt.point(0, globalProfileRow.height / 2), content) } onExited: base.hideTooltip() From a8887406ac213f37968a95c209679b2c06783b72 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 23 Nov 2016 14:05:33 +0100 Subject: [PATCH 400/438] ValidationState checking in MachineManager now properly takes functions into account CURA-2840 --- cura/Settings/MachineManager.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index fce82212cd..108428f740 100644 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -297,6 +297,16 @@ class MachineManager(QObject): changed_validation_state = self._active_container_stack.getProperty(key, property_name) else: changed_validation_state = self._global_container_stack.getProperty(key, property_name) + + if changed_validation_state is None: + # Setting is not validated. This can happen if there is only a setting definition. + # We do need to validate it, because a setting defintions value can be set by a function, which could + # be an invalid setting. + definition = self._active_container_stack.getSettingDefinition(key) + validator_type = UM.Settings.SettingDefinition.getValidatorForType(definition.type) + if validator_type: + validator = validator_type(key) + changed_validation_state = validator(self._active_container_stack) if changed_validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError): self._stacks_have_errors = True self.stacksValidationChanged.emit() From c00775c4889f00cad3dd753c8c3ac15c66d125e5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 23 Nov 2016 16:36:09 +0100 Subject: [PATCH 401/438] Bounding box will no longer sing first part of batman theme CURA-2963 --- cura/CuraApplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index ff70a2e25c..7be20305c4 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -632,7 +632,7 @@ class CuraApplication(QtApplication): if not scene_bounding_box: scene_bounding_box = AxisAlignedBox.Null - if repr(self._scene_bounding_box) != repr(scene_bounding_box): + if repr(self._scene_bounding_box) != repr(scene_bounding_box) and scene_bounding_box.isValid(): self._scene_bounding_box = scene_bounding_box self.sceneBoundingBoxChanged.emit() From 8c35c8fbc39838275732327b0e41516873a2d044 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 23 Nov 2016 17:27:27 +0100 Subject: [PATCH 402/438] Inheritance manager no longer sees settings as setting if they use enum value --- cura/Settings/SettingInheritanceManager.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 7eecdf9591..b13faadd0e 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -167,7 +167,16 @@ class SettingInheritanceManager(QObject): continue if value is not None: # If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value - has_setting_function = isinstance(value, UM.Settings.SettingFunction) and len(value.getUsedSettingKeys()) > 0 + has_setting_function = isinstance(value, UM.Settings.SettingFunction) + if has_setting_function: + for setting_key in value.getUsedSettingKeys(): + if setting_key in self._active_container_stack.getAllKeys(): + break # We found an actual setting. So has_setting_function can remain true + else: + # All of the setting_keys turned out to not be setting keys at all! + # This can happen due enum keys also being marked as settings. + has_setting_function = False + if has_setting_function is False: has_non_function_value = True continue From 29e04bb82510444998884ae37cc4fd309d37a00e Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 23 Nov 2016 17:35:17 +0100 Subject: [PATCH 403/438] Don't change the prime tower size based on prime tower enable The prime tower is properly disabled in the engine nowadays if prime_tower_enable is false. --- resources/definitions/fdmprinter.def.json | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index e33d65dc14..f09ada9214 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3637,7 +3637,6 @@ "unit": "mm", "enabled": "resolveOrValue('prime_tower_enable')", "default_value": 15, - "value": "15 if resolveOrValue('prime_tower_enable') else 0", "resolve": "max(extruderValues('prime_tower_size'))", "minimum_value": "0", "maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)", From 1325c3188244f4f33736a5bb35dd945008323ca2 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 8 Nov 2016 10:45:43 +0100 Subject: [PATCH 404/438] Remove hardcoded container index --- .../MachineSettingsAction.py | 22 ++++++++++++++----- .../MachineSettingsAction.qml | 22 +++++++++---------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index a37aa9c5cb..a6c24232c9 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -1,7 +1,7 @@ # Copyright (c) 2016 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot from cura.MachineAction import MachineAction import cura.Settings.CuraContainerRegistry @@ -19,23 +19,35 @@ class MachineSettingsAction(MachineAction): super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings")) self._qml_url = "MachineSettingsAction.qml" + self._container_index = 0 + cura.Settings.CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded) def _reset(self): global_container_stack = UM.Application.getInstance().getGlobalContainerStack() if global_container_stack: variant = global_container_stack.findContainer({"type": "variant"}) - if variant and variant.getId() == "empty_variant": + if variant: variant_index = global_container_stack.getContainerIndex(variant) - self._createVariant(global_container_stack, variant_index) + if variant_index != self._container_index: + self._container_index = variant_index + self.containerIndexChanged.emit() + if variant.getId() == "empty_variant": + self._createVariant(global_container_stack, self._container_index) - def _createVariant(self, global_container_stack, variant_index): + def _createVariant(self, global_container_stack): # Create and switch to a variant to store the settings in new_variant = UM.Settings.InstanceContainer(global_container_stack.getName() + "_variant") new_variant.addMetaDataEntry("type", "variant") new_variant.setDefinition(global_container_stack.getBottom()) UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant) - global_container_stack.replaceContainer(variant_index, new_variant) + global_container_stack.replaceContainer(self._container_index, new_variant) + + containerIndexChanged = pyqtSignal() + + @pyqtProperty(int, notify = containerIndexChanged) + def containerIndex(self): + return self._container_index def _onContainerAdded(self, container): # Add this action as a supported action to all machine definitions diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index a1d9bcfafd..c90eba1e0a 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -377,7 +377,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_width" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -387,7 +387,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_depth" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -397,7 +397,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_height" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -407,7 +407,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_heated_bed" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -417,7 +417,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_center_is_zero" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -427,7 +427,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_gcode_flavor" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -437,7 +437,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_nozzle_size" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -447,7 +447,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "gantry_height" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -457,7 +457,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_head_with_fans_polygon" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } @@ -468,7 +468,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_start_gcode" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } UM.SettingPropertyProvider @@ -478,7 +478,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_end_gcode" watchedProperties: [ "value" ] - storeIndex: 4 + storeIndex: manager.containerIndex } } \ No newline at end of file From b6689870f520b3649086c3d59b252339ee71ce90 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 8 Nov 2016 13:04:24 +0100 Subject: [PATCH 405/438] Make Machine Settings compatible with machines that use variants (UM2+) For machines that use variants, a second variant container is added between the machine definition and the variant. --- .../MachineSettingsAction.py | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index a6c24232c9..1b26cfcc97 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -21,27 +21,56 @@ class MachineSettingsAction(MachineAction): self._container_index = 0 - cura.Settings.CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded) + self._container_registry = UM.Settings.ContainerRegistry.getInstance() + self._container_registry.containerAdded.connect(self._onContainerAdded) def _reset(self): global_container_stack = UM.Application.getInstance().getGlobalContainerStack() - if global_container_stack: - variant = global_container_stack.findContainer({"type": "variant"}) - if variant: - variant_index = global_container_stack.getContainerIndex(variant) - if variant_index != self._container_index: - self._container_index = variant_index - self.containerIndexChanged.emit() - if variant.getId() == "empty_variant": - self._createVariant(global_container_stack, self._container_index) + if not global_container_stack: + return - def _createVariant(self, global_container_stack): - # Create and switch to a variant to store the settings in - new_variant = UM.Settings.InstanceContainer(global_container_stack.getName() + "_variant") - new_variant.addMetaDataEntry("type", "variant") - new_variant.setDefinition(global_container_stack.getBottom()) - UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant) - global_container_stack.replaceContainer(self._container_index, new_variant) + # First check if there is a variant previously generated by this machine + machine_settings_variant = global_container_stack.findContainer({"type": "variant", "subtype": "machine_settings"}) + if not machine_settings_variant: + # There may be a variant created by the UMOUpgradeSelection machine action + machine_settings_variant = global_container_stack.findContainer({"type": "variant", "id": global_container_stack.getName() + "_variant"}) + + if not machine_settings_variant: + variant = global_container_stack.findContainer({"type": "variant"}) + if variant and variant.getId() == "empty_variant": + # There is an empty variant that we can use to store the machine settings + container_index = global_container_stack.getContainerIndex(variant) + machine_settings_variant = self._createMachineVariant(global_container_stack, container_index) + else: + # Add a second variant before the current variant to store the machine settings + machine_settings_variant = self._createMachineVariant(global_container_stack) + + # Notify the UI in which container to store the machine settings data + container_index = global_container_stack.getContainerIndex(machine_settings_variant) + if container_index != self._container_index: + self._container_index = container_index + self.containerIndexChanged.emit() + + def _createMachineSettingsVariant(self, global_container_stack, container_index = None): + machine_settings_variant = UM.Settings.InstanceContainer(global_container_stack.getName() + "_variant") + if global_container_stack.getMetaDataEntry("has_variants", False): + # If the current machine uses visible variants (eg for nozzle selection), make sure + # not to add this variant to the list. + definition = self._container_registry.findDefinitionContainers(id="fdmprinter")[0] + else: + definition = global_container_stack.getBottom() + machine_settings_variant.setDefinition(definition) + machine_settings_variant.addMetaDataEntry("type", "variant") + machine_settings_variant.addMetaDataEntry("subtype", "machine_settings") + + self._container_registry.addContainer(machine_settings_variant) + + if container_index: + global_container_stack.replaceContainer(container_index, machine_settings_variant) + else: + index = len(global_container_stack.getContainers()) - 1 + global_container_stack.addContainer(machine_settings_variant, index) + return machine_settings_variant containerIndexChanged = pyqtSignal() @@ -56,10 +85,6 @@ class MachineSettingsAction(MachineAction): # Multiextruder printers are not currently supported UM.Logger.log("d", "Not attaching MachineSettingsAction to %s; Multi-extrusion printers are not supported", container.getId()) return - if container.getMetaDataEntry("has_variants", False): - # Machines that use variants are not currently supported - UM.Logger.log("d", "Not attaching MachineSettingsAction to %s; Machines that use variants are not supported", container.getId()) - return UM.Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey()) @@ -90,7 +115,7 @@ class MachineSettingsAction(MachineAction): # Set the material container to a sane default if material_container.getId() == "empty_material": search_criteria = { "type": "material", "definition": "fdmprinter", "id": "*pla*" } - containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria) + containers = self._container_registry.findInstanceContainers(**search_criteria) if containers: global_container_stack.replaceContainer(material_index, containers[0]) else: @@ -99,7 +124,7 @@ class MachineSettingsAction(MachineAction): if "has_materials" in global_container_stack.getMetaData(): global_container_stack.removeMetaDataEntry("has_materials") - empty_material = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0] + empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0] global_container_stack.replaceContainer(material_index, empty_material) UM.Application.getInstance().globalContainerStackChanged.emit() \ No newline at end of file From 5f147b6c784b0caab2ea75e0e68aae94b8fa2f9e Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 8 Nov 2016 13:13:59 +0100 Subject: [PATCH 406/438] Hide nozzle size setting for machines that use variants for nozzle sizes --- plugins/MachineSettingsAction/MachineSettingsAction.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index c90eba1e0a..65968d1817 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -273,17 +273,20 @@ Cura.MachineAction Label { text: catalog.i18nc("@label", "Nozzle size") + visible: !Cura.MachineManager.hasVariants } TextField { id: nozzleSizeField text: machineNozzleSizeProvider.properties.value + visible: !Cura.MachineManager.hasVariants validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ } onEditingFinished: { machineNozzleSizeProvider.setPropertyValue("value", text) } } Label { text: catalog.i18nc("@label", "mm") + visible: !Cura.MachineManager.hasVariants } } } From c8cedb301a3eab52ae8ffd3d23f210e15d36e6bb Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 8 Nov 2016 14:12:05 +0100 Subject: [PATCH 407/438] Use insertContainer instead of adding a parameter to addContainer --- plugins/MachineSettingsAction/MachineSettingsAction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 1b26cfcc97..a965c2cc0a 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -69,7 +69,7 @@ class MachineSettingsAction(MachineAction): global_container_stack.replaceContainer(container_index, machine_settings_variant) else: index = len(global_container_stack.getContainers()) - 1 - global_container_stack.addContainer(machine_settings_variant, index) + global_container_stack.insertContainer(index, machine_settings_variant) return machine_settings_variant containerIndexChanged = pyqtSignal() From 78475d68b34c58655e49894dc0c9a3037afe1217 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 8 Nov 2016 14:32:27 +0100 Subject: [PATCH 408/438] Fix creating variants --- plugins/MachineSettingsAction/MachineSettingsAction.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index a965c2cc0a..8e6ff57b81 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -40,10 +40,10 @@ class MachineSettingsAction(MachineAction): if variant and variant.getId() == "empty_variant": # There is an empty variant that we can use to store the machine settings container_index = global_container_stack.getContainerIndex(variant) - machine_settings_variant = self._createMachineVariant(global_container_stack, container_index) + machine_settings_variant = self._createMachineSettingsVariant(global_container_stack, container_index) else: # Add a second variant before the current variant to store the machine settings - machine_settings_variant = self._createMachineVariant(global_container_stack) + machine_settings_variant = self._createMachineSettingsVariant(global_container_stack) # Notify the UI in which container to store the machine settings data container_index = global_container_stack.getContainerIndex(machine_settings_variant) @@ -68,8 +68,7 @@ class MachineSettingsAction(MachineAction): if container_index: global_container_stack.replaceContainer(container_index, machine_settings_variant) else: - index = len(global_container_stack.getContainers()) - 1 - global_container_stack.insertContainer(index, machine_settings_variant) + global_container_stack.insertContainer(-1, machine_settings_variant) return machine_settings_variant containerIndexChanged = pyqtSignal() From 497b6f99a4bdb067a9194719d8dae7531f1e0e29 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 8 Nov 2016 17:40:27 +0100 Subject: [PATCH 409/438] Fix finding existing variant by id ContainerStack.findContainer cannot find a container by id directly. --- .../MachineSettingsAction.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 8e6ff57b81..9b52a18af2 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -32,18 +32,19 @@ class MachineSettingsAction(MachineAction): # First check if there is a variant previously generated by this machine machine_settings_variant = global_container_stack.findContainer({"type": "variant", "subtype": "machine_settings"}) if not machine_settings_variant: - # There may be a variant created by the UMOUpgradeSelection machine action - machine_settings_variant = global_container_stack.findContainer({"type": "variant", "id": global_container_stack.getName() + "_variant"}) + variant = global_container_stack.findContainer({"type": "variant"}) + if variant: + if variant.getId() == global_container_stack.getName() + "_variant": + # There is a variant created by the UMOUpgradeSelection machine action + machine_settings_variant = variant + if variant.getId() == "empty_variant": + # There is an empty variant that we can replace to store the machine settings + container_index = global_container_stack.getContainerIndex(variant) + machine_settings_variant = self._createMachineSettingsVariant(global_container_stack, container_index) if not machine_settings_variant: - variant = global_container_stack.findContainer({"type": "variant"}) - if variant and variant.getId() == "empty_variant": - # There is an empty variant that we can use to store the machine settings - container_index = global_container_stack.getContainerIndex(variant) - machine_settings_variant = self._createMachineSettingsVariant(global_container_stack, container_index) - else: - # Add a second variant before the current variant to store the machine settings - machine_settings_variant = self._createMachineSettingsVariant(global_container_stack) + # Add a new variant to store the machine settings + machine_settings_variant = self._createMachineSettingsVariant(global_container_stack) # Notify the UI in which container to store the machine settings data container_index = global_container_stack.getContainerIndex(machine_settings_variant) From edf4589150f6e41e93f3a0f33730dac97bf8c8a4 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 9 Nov 2016 11:44:38 +0100 Subject: [PATCH 410/438] Use fixed width font and no wrapping in start/end gcode editor --- plugins/MachineSettingsAction/MachineSettingsAction.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index 65968d1817..f6b20a648a 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -311,6 +311,8 @@ Cura.MachineAction id: machineStartGcodeField width: parent.width height: parent.height - y + font: UM.Theme.getFont("fixed") + wrapMode: TextEdit.NoWrap text: machineStartGcodeProvider.properties.value onActiveFocusChanged: { @@ -333,6 +335,8 @@ Cura.MachineAction id: machineEndGcodeField width: parent.width height: parent.height - y + font: UM.Theme.getFont("fixed") + wrapMode: TextEdit.NoWrap text: machineEndGcodeProvider.properties.value onActiveFocusChanged: { From 9e048aa6adf20afe71ec6fd32ea158304221a539 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 9 Nov 2016 15:29:23 +0100 Subject: [PATCH 411/438] Get gcode flavor options from propertyprovider instead of hardcoding --- .../MachineSettingsAction.qml | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index f6b20a648a..ea299438f0 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -147,19 +147,40 @@ Cura.MachineAction ComboBox { - model: ["RepRap (Marlin/Sprinter)", "UltiGCode", "Repetier"] + model: ListModel + { + id: flavorModel + Component.onCompleted: + { + // Options come in as a string-representation of an OrderedDict + var options = machineGCodeFlavorProvider.properties.options.match(/^OrderedDict\(\[\((.*)\)\]\)$/); + if(options) + { + options = options[1].split("), (") + for(var i = 0; i < options.length; i++) + { + var option = options[i].substring(1, options[i].length - 1).split("', '") + flavorModel.append({text: option[1], value: option[0]}); + } + } + } + } currentIndex: { - var index = model.indexOf(machineGCodeFlavorProvider.properties.value); - if(index == -1) + var currentValue = machineGCodeFlavorProvider.properties.value; + var index = 0; + for(var i = 0; i < flavorModel.count; i++) { - index = 0; + if(flavorModel.get(i).value == currentValue) { + index = i; + break; + } } return index } onActivated: { - machineGCodeFlavorProvider.setPropertyValue("value", model[index]); + machineGCodeFlavorProvider.setPropertyValue("value", flavorModel.get(index).value); manager.updateHasMaterialsMetadata(); } } @@ -433,7 +454,7 @@ Cura.MachineAction containerStackId: Cura.MachineManager.activeMachineId key: "machine_gcode_flavor" - watchedProperties: [ "value" ] + watchedProperties: [ "value", "options" ] storeIndex: manager.containerIndex } From 21bcb0e434b85c6e05cfd3b9b0410240d18e396d Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 23 Nov 2016 22:53:03 +0100 Subject: [PATCH 412/438] Create a definition_changes container instead of a second variant --- cura/CuraApplication.py | 2 + .../MachineSettingsAction.py | 50 ++++++------------- .../UMOUpgradeSelection.py | 34 +++++++------ 3 files changed, 36 insertions(+), 50 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7be20305c4..e8c8ba981a 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -380,6 +380,8 @@ class CuraApplication(QtApplication): path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name) elif instance_type == "variant": path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name) + elif instance_type == "definition_changes": + path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name) if path: instance.setPath(path) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 9b52a18af2..c3a7f50952 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -4,11 +4,11 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot from cura.MachineAction import MachineAction -import cura.Settings.CuraContainerRegistry import UM.Application import UM.Settings.InstanceContainer import UM.Settings.DefinitionContainer +import UM.Settings.ContainerRegistry import UM.Logger import UM.i18n @@ -29,48 +29,28 @@ class MachineSettingsAction(MachineAction): if not global_container_stack: return - # First check if there is a variant previously generated by this machine - machine_settings_variant = global_container_stack.findContainer({"type": "variant", "subtype": "machine_settings"}) - if not machine_settings_variant: - variant = global_container_stack.findContainer({"type": "variant"}) - if variant: - if variant.getId() == global_container_stack.getName() + "_variant": - # There is a variant created by the UMOUpgradeSelection machine action - machine_settings_variant = variant - if variant.getId() == "empty_variant": - # There is an empty variant that we can replace to store the machine settings - container_index = global_container_stack.getContainerIndex(variant) - machine_settings_variant = self._createMachineSettingsVariant(global_container_stack, container_index) - - if not machine_settings_variant: - # Add a new variant to store the machine settings - machine_settings_variant = self._createMachineSettingsVariant(global_container_stack) + # Make sure there is a definition_changes container to store the machine settings + definition_changes_container = global_container_stack.findContainer({"type": "definition_changes"}) + if not definition_changes_container: + definition_changes_container = self._createDefinitionChangesContainer(global_container_stack) # Notify the UI in which container to store the machine settings data - container_index = global_container_stack.getContainerIndex(machine_settings_variant) + container_index = global_container_stack.getContainerIndex(definition_changes_container) if container_index != self._container_index: self._container_index = container_index self.containerIndexChanged.emit() - def _createMachineSettingsVariant(self, global_container_stack, container_index = None): - machine_settings_variant = UM.Settings.InstanceContainer(global_container_stack.getName() + "_variant") - if global_container_stack.getMetaDataEntry("has_variants", False): - # If the current machine uses visible variants (eg for nozzle selection), make sure - # not to add this variant to the list. - definition = self._container_registry.findDefinitionContainers(id="fdmprinter")[0] - else: - definition = global_container_stack.getBottom() - machine_settings_variant.setDefinition(definition) - machine_settings_variant.addMetaDataEntry("type", "variant") - machine_settings_variant.addMetaDataEntry("subtype", "machine_settings") + def _createDefinitionChangesContainer(self, global_container_stack, container_index = None): + definition_changes_container = UM.Settings.InstanceContainer(global_container_stack.getName() + "_settings") + definition = global_container_stack.getBottom() + definition_changes_container.setDefinition(definition) + definition_changes_container.addMetaDataEntry("type", "definition_changes") - self._container_registry.addContainer(machine_settings_variant) + self._container_registry.addContainer(definition_changes_container) + # Insert definition_changes between the definition and the variant + global_container_stack.insertContainer(-1, definition_changes_container) - if container_index: - global_container_stack.replaceContainer(container_index, machine_settings_variant) - else: - global_container_stack.insertContainer(-1, machine_settings_variant) - return machine_settings_variant + return definition_changes_container containerIndexChanged = pyqtSignal() diff --git a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py index b92dc30c68..e85ec7b434 100644 --- a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py +++ b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py @@ -27,19 +27,23 @@ class UMOUpgradeSelection(MachineAction): def setHeatedBed(self, heated_bed = True): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: - variant = global_container_stack.findContainer({"type": "variant"}) - if variant: - if variant.getId() == "empty_variant": - variant_index = global_container_stack.getContainerIndex(variant) - variant = self._createVariant(global_container_stack, variant_index) - variant.setProperty("machine_heated_bed", "value", heated_bed) - self.heatedBedChanged.emit() + # Make sure there is a definition_changes container to store the machine settings + definition_changes_container = global_container_stack.findContainer({"type": "definition_changes"}) + if not definition_changes_container: + definition_changes_container = self._createDefinitionChangesContainer(global_container_stack) - def _createVariant(self, global_container_stack, variant_index): - # Create and switch to a variant to store the settings in - new_variant = UM.Settings.InstanceContainer(global_container_stack.getName() + "_variant") - new_variant.addMetaDataEntry("type", "variant") - new_variant.setDefinition(global_container_stack.getBottom()) - UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant) - global_container_stack.replaceContainer(variant_index, new_variant) - return new_variant \ No newline at end of file + definition_changes_container.setProperty("machine_heated_bed", "value", heated_bed) + self.heatedBedChanged.emit() + + def _createDefinitionChangesContainer(self, global_container_stack): + # Create a definition_changes container to store the settings in and add it to the stack + definition_changes_container = UM.Settings.InstanceContainer(global_container_stack.getName() + "_settings") + definition = global_container_stack.getBottom() + definition_changes_container.setDefinition(definition) + definition_changes_container.addMetaDataEntry("type", "definition_changes") + + UM.Settings.ContainerRegistry.getInstance().addContainer(definition_changes_container) + # Insert definition_changes between the definition and the variant + global_container_stack.insertContainer(-1, definition_changes_container) + + return definition_changes_container From 9dd8a88602a1bf191c68fba4211846d26a214987 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 24 Nov 2016 10:12:11 +0100 Subject: [PATCH 413/438] Fix review issues --- resources/qml/Settings/SettingView.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index c3da68bf79..5f20f92b20 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -39,7 +39,7 @@ Item border.width: UM.Theme.getSize("default_lining").width border.color: { - if(mouseArea.containsMouse || clearFilterButton.containsMouse) + if(hoverMouseArea.containsMouse || clearFilterButton.containsMouse) { return UM.Theme.getColor("setting_control_border_highlight"); } @@ -65,7 +65,6 @@ Item TextField { id: filter; - visible: !monitoringPrint anchors.left: parent.left anchors.right: clearFilterButton.left @@ -114,7 +113,7 @@ Item MouseArea { - id: mouseArea + id: hoverMouseArea anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton From 44f309226ab693394286f76d3dbddf7243f080be Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 24 Nov 2016 10:19:49 +0100 Subject: [PATCH 414/438] Make initial layer speed scale with normal speed All profiles that set the print speed are edited as well to make sure that the initial layer speed is still the same. Contributes to issue #1170. --- resources/definitions/fdmprinter.def.json | 1 + resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg | 1 + resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg | 1 + resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg | 1 + resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg | 1 + .../quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg | 1 + .../quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg | 1 + resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg | 1 + resources/variants/ultimaker3_aa04.inst.cfg | 1 + resources/variants/ultimaker3_extended_aa04.inst.cfg | 1 + 41 files changed, 41 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index f09ada9214..abe4bf7a3d 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1696,6 +1696,7 @@ "unit": "mm/s", "type": "float", "default_value": 30, + "value": "speed_print * 30 / 60", "minimum_value": "0.1", "maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)", "maximum_value_warning": "300", diff --git a/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg index 868f1dc016..015f3b4e43 100644 --- a/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg @@ -15,5 +15,6 @@ wall_thickness = 0.88 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 30 +speed_layer_0 = =math.round(speed_print * 30 / 30) cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg index b17a1f2a6a..b403bed502 100644 --- a/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 0.7 top_bottom_thickness = 0.75 infill_sparse_density = 18 speed_print = 60 +speed_layer_0 = =math.round(speed_print * 30 / 60) speed_wall = 50 speed_topbottom = 30 speed_travel = 150 diff --git a/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg index c2b15d1074..ac7cd104da 100644 --- a/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 50 +speed_layer_0 = =math.round(speed_print * 30 / 50) speed_topbottom = 20 cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg index 9a84f5c04a..10008f1b94 100644 --- a/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.8 infill_sparse_density = 20 speed_print = 50 +speed_layer_0 = =math.round(speed_print * 30 / 50) speed_topbottom = 20 cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg index fd4c2c120a..8618248aa5 100644 --- a/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.59 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 55 +speed_layer_0 = =math.round(speed_print * 30 / 55) speed_wall = 40 speed_wall_0 = 25 speed_topbottom = 20 diff --git a/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg index cd4c591640..47a55ff426 100644 --- a/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 2.1 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 +speed_layer_0 = =math.round(speed_print * 30 / 40) speed_wall_0 = 25 cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg index 94ddd58081..41eb89a3c3 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 0.88 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 30 +speed_layer_0 = =math.round(speed_print * 30 / 30) cool_min_layer_time = 3 cool_fan_speed_min = 20 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg index 8bcb3efee4..018abf115a 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 0.7 top_bottom_thickness = 0.75 infill_sparse_density = 18 speed_print = 55 +speed_layer_0 = =math.round(speed_print * 30 / 55) speed_wall = 40 speed_topbottom = 30 speed_travel = 150 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg index c6ea33da2d..672fd71aff 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 45 +speed_layer_0 = =math.round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 3 cool_fan_speed_min = 20 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg index ba7886276b..b2426452b4 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.8 infill_sparse_density = 20 speed_print = 45 +speed_layer_0 = =math.round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 3 cool_fan_speed_min = 20 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg index 3a0af33a6a..112efc1418 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.59 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 +speed_layer_0 = =math.round(speed_print * 30 / 40) speed_infill = 55 cool_min_layer_time = 3 cool_fan_speed_min = 50 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg index e8885fc73a..b7bbe880c8 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 2.1 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 +speed_layer_0 = =math.round(speed_print * 30 / 40) cool_min_layer_time = 3 cool_fan_speed_min = 50 cool_min_speed = 15 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg index b86bb877f6..bb60b862af 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 0.88 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 30 +speed_layer_0 = =math.round(speed_print * 30 / 30) cool_min_layer_time = 2 cool_fan_speed_min = 20 cool_min_speed = 15 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg index 8f8fb9e01b..5f2bfc377b 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 0.7 top_bottom_thickness = 0.75 infill_sparse_density = 18 speed_print = 45 +speed_layer_0 = =math.round(speed_print * 30 / 45) speed_wall = 40 speed_travel = 150 speed_layer_0 = 30 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg index bb6a1ee079..e0e2efd77c 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 45 +speed_layer_0 = =math.round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 2 cool_fan_speed_min = 80 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg index 54122164da..4438706ccb 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.8 infill_sparse_density = 20 speed_print = 45 +speed_layer_0 = =math.round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 3 cool_fan_speed_min = 80 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg index 00a6160f46..a3391db13f 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 1.59 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 +speed_layer_0 = =math.round(speed_print * 30 / 40) cool_min_layer_time = 5 cool_fan_speed_min = 80 cool_min_speed = 8 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg index 83714ca40a..a6360d7ec6 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg @@ -15,6 +15,7 @@ wall_thickness = 2.1 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 +speed_layer_0 = =math.round(speed_print * 30 / 40) cool_min_layer_time = 3 cool_fan_speed_min = 80 cool_min_speed = 8 diff --git a/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg index be6e962949..cdb529dbd4 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg @@ -40,6 +40,7 @@ line_width = 0.57 layer_0_z_overlap = 0.22 raft_base_line_width = 1.2 speed_print = 25 +speed_layer_0 = =math.round(speed_print * 30 / 50) support_line_distance = 2.85 support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg index 2cd6ef9dac..01195eb5b4 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg @@ -33,6 +33,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 25 +speed_layer_0 = =math.round(speed_print * 30 / 25) speed_wall_0 = 20 support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg index f243637cd7..4cdff424e3 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg @@ -33,6 +33,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 30 +speed_layer_0 = =math.round(speed_print * 30 / 30) speed_wall_0 = 20 support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg index 023ffd7498..6e2ce19aec 100644 --- a/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg @@ -41,6 +41,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.25 raft_base_line_width = 1.6 speed_print = 55 +speed_layer_0 = =math.round(speed_print * 30 / 55) support_angle = 45 raft_interface_line_spacing = 1.8 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg index 91e75c2450..168cf92a09 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg @@ -31,6 +31,7 @@ infill_sparse_density = 25 layer_0_z_overlap = 0.22 cool_min_layer_time = 2 speed_print = 30 +speed_layer_0 = =math.round(speed_print * 30 / 30) raft_base_line_spacing = 1 raft_base_line_width = 0.5 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg index a5df9972b9..55ece1b133 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg @@ -31,6 +31,7 @@ infill_sparse_density = 25 layer_0_z_overlap = 0.22 cool_min_layer_time = 2 speed_print = 30 +speed_layer_0 = =math.round(speed_print * 30 / 30) raft_base_line_spacing = 1 raft_base_line_width = 0.5 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg index e05cbb0dd0..4b380c0321 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg @@ -31,6 +31,7 @@ infill_sparse_density = 30 layer_0_z_overlap = 0.22 cool_min_layer_time = 3 speed_print = 45 +speed_layer_0 = =math.round(speed_print * 30 / 45) support_angle = 45 raft_base_line_spacing = 1.6 raft_base_line_width = 0.8 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg index 0b79ed29bd..ec6cb89cd7 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg @@ -31,6 +31,7 @@ infill_sparse_density = 30 layer_0_z_overlap = 0.22 cool_min_layer_time = 3 speed_print = 45 +speed_layer_0 = =math.round(speed_print * 30 / 45) support_angle = 45 raft_base_line_spacing = 1.6 raft_base_line_width = 0.8 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg index 52e61a8145..50e0e41b24 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg @@ -32,6 +32,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 40 +speed_layer_0 = =math.round(speed_print * 30 / 40) support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg index 383fe8722f..3bae6b4271 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg @@ -32,6 +32,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 40 +speed_layer_0 = =math.round(speed_print * 30 / 40) support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg index 6a1b477165..ca62cff441 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg @@ -16,6 +16,7 @@ material_print_temperature = 240 prime_tower_size = 16 skin_overlap = 20 speed_print = 60 +speed_layer_0 = =math.round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 35 / 60) speed_wall = =math.ceil(speed_print * 45 / 60) speed_wall_0 = =math.ceil(speed_wall * 35 / 45) diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg index 3b5d37024d..4f6861608c 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg @@ -17,6 +17,7 @@ material_print_temperature = 235 material_standby_temperature = 100 prime_tower_size = 16 speed_print = 60 +speed_layer_0 = =math.round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 30 / 60) speed_wall = =math.ceil(speed_print * 40 / 60) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg index dcb8e85563..f22d58d389 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg @@ -16,6 +16,7 @@ machine_nozzle_heat_up_speed = 1.5 material_standby_temperature = 100 prime_tower_size = 16 speed_print = 50 +speed_layer_0 = =math.round(speed_print * 30 / 50) speed_topbottom = =math.ceil(speed_print * 30 / 50) speed_wall = =math.ceil(speed_print * 30 / 50) diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg index 4e99ac446e..8604597d93 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg @@ -16,6 +16,7 @@ material_print_temperature = 230 material_standby_temperature = 100 prime_tower_size = 16 speed_print = 55 +speed_layer_0 = =math.round(speed_print * 30 / 55) speed_topbottom = =math.ceil(speed_print * 30 / 55) speed_wall = =math.ceil(speed_print * 30 / 55) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg index d7e0ed62b6..b7a9097188 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg @@ -15,6 +15,7 @@ material_standby_temperature = 100 prime_tower_size = 17 skin_overlap = 20 speed_print = 60 +speed_layer_0 = =math.round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 35 / 60) speed_wall = =math.ceil(speed_print * 45 / 60) speed_wall_0 = =math.ceil(speed_wall * 35 / 45) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg index 5717bf50fe..c11bbfa17d 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg @@ -15,6 +15,7 @@ material_print_temperature = 245 material_standby_temperature = 100 prime_tower_size = 17 speed_print = 60 +speed_layer_0 = =math.round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 30 / 60) speed_wall = =math.ceil(speed_print * 40 / 60) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg index e058ef8cac..6526874b8e 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg @@ -16,6 +16,7 @@ machine_nozzle_heat_up_speed = 1.5 material_standby_temperature = 100 prime_tower_size = 17 speed_print = 50 +speed_layer_0 = =math.round(speed_print * 30 / 50) speed_topbottom = =math.ceil(speed_print * 30 / 50) speed_wall = =math.ceil(speed_print * 30 / 50) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg index 1ccd1c54d3..4db54e2220 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg @@ -16,6 +16,7 @@ material_print_temperature = 240 material_standby_temperature = 100 prime_tower_size = 17 speed_print = 55 +speed_layer_0 = =math.round(speed_print * 30 / 55) speed_topbottom = =math.ceil(speed_print * 30 / 55) speed_wall = =math.ceil(speed_print * 30 / 55) diff --git a/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg index 3f2aa1e652..d778d8c373 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg @@ -17,6 +17,7 @@ machine_nozzle_heat_up_speed = 1.6 material_standby_temperature = 100 prime_tower_enable = False speed_print = 80 +speed_layer_0 = =math.round(speed_print * 30 / 80) speed_topbottom = =math.ceil(speed_print * 30 / 80) speed_wall = =math.ceil(speed_print * 40 / 80) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) diff --git a/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg index f71c51e7ff..a38df02f6e 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg @@ -19,6 +19,7 @@ material_print_temperature = 195 material_standby_temperature = 100 skin_overlap = 10 speed_print = 60 +speed_layer_0 = =math.round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 30 / 60) speed_wall = =math.ceil(speed_print * 30 / 60) top_bottom_thickness = 1 diff --git a/resources/variants/ultimaker3_aa04.inst.cfg b/resources/variants/ultimaker3_aa04.inst.cfg index c8d5b8aceb..41967739b4 100644 --- a/resources/variants/ultimaker3_aa04.inst.cfg +++ b/resources/variants/ultimaker3_aa04.inst.cfg @@ -28,6 +28,7 @@ retraction_min_travel = =line_width * 2 retraction_prime_speed = =retraction_speed skin_overlap = 15 speed_print = 70 +speed_layer_0 = =speed_print * 30 / 70 speed_topbottom = =math.ceil(speed_print * 30 / 70) speed_wall = =math.ceil(speed_print * 30 / 70) support_angle = 60 diff --git a/resources/variants/ultimaker3_extended_aa04.inst.cfg b/resources/variants/ultimaker3_extended_aa04.inst.cfg index 24cbf04a26..69c800532d 100644 --- a/resources/variants/ultimaker3_extended_aa04.inst.cfg +++ b/resources/variants/ultimaker3_extended_aa04.inst.cfg @@ -27,6 +27,7 @@ retraction_min_travel = 1.5 retraction_prime_speed = 25 skin_overlap = 15 speed_print = 70 +speed_layer_0 = =math.round(speed_print * 30 / 70) speed_topbottom = =math.ceil(speed_print * 30 / 70) speed_wall = =math.ceil(speed_print * 30 / 70) support_angle = 60 From a8d5537487f2e3fc2022de9922cfdd432566657e Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 24 Nov 2016 11:19:46 +0100 Subject: [PATCH 415/438] Fix XmlMaterialProfile setProperty in correct container. Contributes to CURA-2861 --- plugins/XmlMaterialProfile/XmlMaterialProfile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 30d1307d01..e3ed28e94d 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -413,7 +413,7 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): for entry in settings: key = entry.get("key") if key in self.__material_property_setting_map: - self.setProperty(self.__material_property_setting_map[key], "value", entry.text, self._definition) + self.setProperty(self.__material_property_setting_map[key], "value", entry.text) global_setting_values[self.__material_property_setting_map[key]] = entry.text elif key in self.__unmapped_settings: if key == "hardware compatible": @@ -463,10 +463,10 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): new_material.getMetaData()["compatible"] = machine_compatibility for key, value in global_setting_values.items(): - new_material.setProperty(key, "value", value, definition) + new_material.setProperty(key, "value", value) for key, value in machine_setting_values.items(): - new_material.setProperty(key, "value", value, definition) + new_material.setProperty(key, "value", value) new_material._dirty = False @@ -510,13 +510,13 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): new_hotend_material.getMetaData()["compatible"] = hotend_compatibility for key, value in global_setting_values.items(): - new_hotend_material.setProperty(key, "value", value, definition) + new_hotend_material.setProperty(key, "value", value) for key, value in machine_setting_values.items(): - new_hotend_material.setProperty(key, "value", value, definition) + new_hotend_material.setProperty(key, "value", value) for key, value in hotend_setting_values.items(): - new_hotend_material.setProperty(key, "value", value, definition) + new_hotend_material.setProperty(key, "value", value) new_hotend_material._dirty = False UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material) From d045738dc86de7821b67f482b5628dc52f88ebc0 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 24 Nov 2016 11:51:35 +0100 Subject: [PATCH 416/438] Simplify button width logic --- resources/qml/Sidebar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index c2e6737ad4..9c0d641d77 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -214,7 +214,7 @@ Rectangle anchors.left: parent.left anchors.leftMargin: model.index * (settingsModeSelection.width / 2) anchors.verticalCenter: parent.verticalCenter - width: 0.5 * parent.width - (modesListModel.get(index).showFilterButton ? toggleFilterButton.width : 0) + width: 0.5 * parent.width - (model.showFilterButton ? toggleFilterButton.width : 0) text: model.text exclusiveGroup: modeMenuGroup; checkable: true; From 072d33ec684e1f3ef1a58027c4f70314df721cd8 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 24 Nov 2016 12:58:33 +0100 Subject: [PATCH 417/438] Fixed profiles by removing double entries and changing math.round to round. --- resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg | 3 +-- resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg | 2 +- resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg | 2 +- .../quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg | 3 +-- resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg | 2 +- .../quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg | 3 +-- resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg | 3 +-- resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg | 2 +- .../quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg | 2 +- .../quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg | 3 +-- resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg | 2 +- resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg | 2 +- .../quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg | 2 +- resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg | 2 +- .../quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg | 2 +- .../quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg | 2 +- .../quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg | 2 +- resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg | 2 +- .../quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg | 2 +- .../quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg | 2 +- resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg | 2 +- .../quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg | 2 +- resources/variants/ultimaker3_extended_aa04.inst.cfg | 2 +- 39 files changed, 39 insertions(+), 44 deletions(-) diff --git a/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg index 015f3b4e43..db2b48b3cc 100644 --- a/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.25_normal.inst.cfg @@ -15,6 +15,6 @@ wall_thickness = 0.88 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 30 -speed_layer_0 = =math.round(speed_print * 30 / 30) +speed_layer_0 = =round(speed_print * 30 / 30) cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg index b403bed502..d3f2740202 100644 --- a/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.4_fast.inst.cfg @@ -15,10 +15,9 @@ wall_thickness = 0.7 top_bottom_thickness = 0.75 infill_sparse_density = 18 speed_print = 60 -speed_layer_0 = =math.round(speed_print * 30 / 60) +speed_layer_0 = =round(speed_print * 30 / 60) speed_wall = 50 speed_topbottom = 30 speed_travel = 150 -speed_layer_0 = 30 cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg index ac7cd104da..d3347b4712 100644 --- a/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.4_high.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 50 -speed_layer_0 = =math.round(speed_print * 30 / 50) +speed_layer_0 = =round(speed_print * 30 / 50) speed_topbottom = 20 cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg index 10008f1b94..758225535a 100644 --- a/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.4_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.8 infill_sparse_density = 20 speed_print = 50 -speed_layer_0 = =math.round(speed_print * 30 / 50) +speed_layer_0 = =round(speed_print * 30 / 50) speed_topbottom = 20 cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg index 8618248aa5..5eed5965e4 100644 --- a/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.6_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.59 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 55 -speed_layer_0 = =math.round(speed_print * 30 / 55) +speed_layer_0 = =round(speed_print * 30 / 55) speed_wall = 40 speed_wall_0 = 25 speed_topbottom = 20 diff --git a/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg index 47a55ff426..96a81d874e 100644 --- a/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/pla_0.8_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 2.1 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 -speed_layer_0 = =math.round(speed_print * 30 / 40) +speed_layer_0 = =round(speed_print * 30 / 40) speed_wall_0 = 25 cool_min_layer_time = 5 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg index 41eb89a3c3..afe7c52f1a 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.25_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 0.88 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 30 -speed_layer_0 = =math.round(speed_print * 30 / 30) +speed_layer_0 = =round(speed_print * 30 / 30) cool_min_layer_time = 3 cool_fan_speed_min = 20 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg index 018abf115a..4eff2c3d91 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.4_fast.inst.cfg @@ -15,11 +15,10 @@ wall_thickness = 0.7 top_bottom_thickness = 0.75 infill_sparse_density = 18 speed_print = 55 -speed_layer_0 = =math.round(speed_print * 30 / 55) +speed_layer_0 = =round(speed_print * 30 / 55) speed_wall = 40 speed_topbottom = 30 speed_travel = 150 -speed_layer_0 = 30 cool_min_layer_time = 3 cool_fan_speed_min = 20 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg index 672fd71aff..607598b249 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.4_high.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 45 -speed_layer_0 = =math.round(speed_print * 30 / 45) +speed_layer_0 = =round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 3 cool_fan_speed_min = 20 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg index b2426452b4..be379beb30 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.4_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.8 infill_sparse_density = 20 speed_print = 45 -speed_layer_0 = =math.round(speed_print * 30 / 45) +speed_layer_0 = =round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 3 cool_fan_speed_min = 20 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg index 112efc1418..b988738273 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.6_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.59 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 -speed_layer_0 = =math.round(speed_print * 30 / 40) +speed_layer_0 = =round(speed_print * 30 / 40) speed_infill = 55 cool_min_layer_time = 3 cool_fan_speed_min = 50 diff --git a/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg index b7bbe880c8..c6954c92d8 100644 --- a/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_abs_0.8_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 2.1 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 -speed_layer_0 = =math.round(speed_print * 30 / 40) +speed_layer_0 = =round(speed_print * 30 / 40) cool_min_layer_time = 3 cool_fan_speed_min = 50 cool_min_speed = 15 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg index bb60b862af..0128800950 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.25_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 0.88 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 30 -speed_layer_0 = =math.round(speed_print * 30 / 30) +speed_layer_0 = =round(speed_print * 30 / 30) cool_min_layer_time = 2 cool_fan_speed_min = 20 cool_min_speed = 15 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg index 5f2bfc377b..0c3fec0afa 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_fast.inst.cfg @@ -15,10 +15,9 @@ wall_thickness = 0.7 top_bottom_thickness = 0.75 infill_sparse_density = 18 speed_print = 45 -speed_layer_0 = =math.round(speed_print * 30 / 45) +speed_layer_0 = =round(speed_print * 30 / 45) speed_wall = 40 speed_travel = 150 -speed_layer_0 = 30 cool_min_layer_time = 3 cool_fan_speed_min = 80 cool_min_speed = 10 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg index e0e2efd77c..597d450bd4 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_high.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.72 infill_sparse_density = 22 speed_print = 45 -speed_layer_0 = =math.round(speed_print * 30 / 45) +speed_layer_0 = =round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 2 cool_fan_speed_min = 80 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg index 4438706ccb..1d624aeb33 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.4_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.05 top_bottom_thickness = 0.8 infill_sparse_density = 20 speed_print = 45 -speed_layer_0 = =math.round(speed_print * 30 / 45) +speed_layer_0 = =round(speed_print * 30 / 45) speed_wall = 30 cool_min_layer_time = 3 cool_fan_speed_min = 80 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg index a3391db13f..c9c9fbf88c 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.6_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 1.59 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 -speed_layer_0 = =math.round(speed_print * 30 / 40) +speed_layer_0 = =round(speed_print * 30 / 40) cool_min_layer_time = 5 cool_fan_speed_min = 80 cool_min_speed = 8 diff --git a/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg index a6360d7ec6..9f02a97a36 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpe_0.8_normal.inst.cfg @@ -15,7 +15,7 @@ wall_thickness = 2.1 top_bottom_thickness = 1.2 infill_sparse_density = 20 speed_print = 40 -speed_layer_0 = =math.round(speed_print * 30 / 40) +speed_layer_0 = =round(speed_print * 30 / 40) cool_min_layer_time = 3 cool_fan_speed_min = 80 cool_min_speed = 8 diff --git a/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg index cdb529dbd4..a8d90b65ef 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpep_0.6_draft.inst.cfg @@ -20,7 +20,6 @@ raft_surface_thickness = 0.2 raft_surface_line_width = 0.57 raft_interface_line_spacing = 1.4 raft_margin = 15 -speed_layer_0 = 30 raft_airgap = 0.37 infill_overlap = 5 layer_height = 0.3 @@ -40,7 +39,7 @@ line_width = 0.57 layer_0_z_overlap = 0.22 raft_base_line_width = 1.2 speed_print = 25 -speed_layer_0 = =math.round(speed_print * 30 / 50) +speed_layer_0 = =round(speed_print * 30 / 50) support_line_distance = 2.85 support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg index 01195eb5b4..a16708e4ff 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_draft.inst.cfg @@ -33,7 +33,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 25 -speed_layer_0 = =math.round(speed_print * 30 / 25) +speed_layer_0 = =round(speed_print * 30 / 25) speed_wall_0 = 20 support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg index 4cdff424e3..0cd03af871 100644 --- a/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_cpep_0.8_normal.inst.cfg @@ -33,7 +33,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 30 -speed_layer_0 = =math.round(speed_print * 30 / 30) +speed_layer_0 = =round(speed_print * 30 / 30) speed_wall_0 = 20 support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg index 6e2ce19aec..223e42291e 100644 --- a/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_nylon_0.8_normal.inst.cfg @@ -20,7 +20,6 @@ support_top_distance = 0.5 raft_surface_thickness = 0.2 wall_thickness = 2.4 raft_margin = 15 -speed_layer_0 = 30 raft_airgap = 0.44 infill_overlap = 5 layer_height = 0.2 @@ -41,7 +40,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.25 raft_base_line_width = 1.6 speed_print = 55 -speed_layer_0 = =math.round(speed_print * 30 / 55) +speed_layer_0 = =round(speed_print * 30 / 55) support_angle = 45 raft_interface_line_spacing = 1.8 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg index 168cf92a09..a70d82d909 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.25_high.inst.cfg @@ -31,7 +31,7 @@ infill_sparse_density = 25 layer_0_z_overlap = 0.22 cool_min_layer_time = 2 speed_print = 30 -speed_layer_0 = =math.round(speed_print * 30 / 30) +speed_layer_0 = =round(speed_print * 30 / 30) raft_base_line_spacing = 1 raft_base_line_width = 0.5 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg index 55ece1b133..b3ba1ecf8c 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.25_normal.inst.cfg @@ -31,7 +31,7 @@ infill_sparse_density = 25 layer_0_z_overlap = 0.22 cool_min_layer_time = 2 speed_print = 30 -speed_layer_0 = =math.round(speed_print * 30 / 30) +speed_layer_0 = =round(speed_print * 30 / 30) raft_base_line_spacing = 1 raft_base_line_width = 0.5 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg index 4b380c0321..a9b8418bcb 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.4_fast.inst.cfg @@ -31,7 +31,7 @@ infill_sparse_density = 30 layer_0_z_overlap = 0.22 cool_min_layer_time = 3 speed_print = 45 -speed_layer_0 = =math.round(speed_print * 30 / 45) +speed_layer_0 = =round(speed_print * 30 / 45) support_angle = 45 raft_base_line_spacing = 1.6 raft_base_line_width = 0.8 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg index ec6cb89cd7..e111597c2d 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.4_normal.inst.cfg @@ -31,7 +31,7 @@ infill_sparse_density = 30 layer_0_z_overlap = 0.22 cool_min_layer_time = 3 speed_print = 45 -speed_layer_0 = =math.round(speed_print * 30 / 45) +speed_layer_0 = =round(speed_print * 30 / 45) support_angle = 45 raft_base_line_spacing = 1.6 raft_base_line_width = 0.8 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg index 50e0e41b24..cd0d3b3695 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.8_draft.inst.cfg @@ -32,7 +32,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 40 -speed_layer_0 = =math.round(speed_print * 30 / 40) +speed_layer_0 = =round(speed_print * 30 / 40) support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg b/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg index 3bae6b4271..bdd774824e 100644 --- a/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg +++ b/resources/quality/ultimaker2_plus/um2p_pc_0.8_normal.inst.cfg @@ -32,7 +32,7 @@ infill_sparse_density = 40 layer_0_z_overlap = 0.22 raft_base_line_width = 1.6 speed_print = 40 -speed_layer_0 = =math.round(speed_print * 30 / 40) +speed_layer_0 = =round(speed_print * 30 / 40) support_angle = 45 cool_min_layer_time = 3 diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg index ca62cff441..9ea0c2119f 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_Draft_Print.inst.cfg @@ -16,7 +16,7 @@ material_print_temperature = 240 prime_tower_size = 16 skin_overlap = 20 speed_print = 60 -speed_layer_0 = =math.round(speed_print * 30 / 60) +speed_layer_0 = =round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 35 / 60) speed_wall = =math.ceil(speed_print * 45 / 60) speed_wall_0 = =math.ceil(speed_wall * 35 / 45) diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg index 4f6861608c..d5c012d0c6 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_Fast_Print.inst.cfg @@ -17,7 +17,7 @@ material_print_temperature = 235 material_standby_temperature = 100 prime_tower_size = 16 speed_print = 60 -speed_layer_0 = =math.round(speed_print * 30 / 60) +speed_layer_0 = =round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 30 / 60) speed_wall = =math.ceil(speed_print * 40 / 60) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg index f22d58d389..10651f520c 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_High_Quality.inst.cfg @@ -16,7 +16,7 @@ machine_nozzle_heat_up_speed = 1.5 material_standby_temperature = 100 prime_tower_size = 16 speed_print = 50 -speed_layer_0 = =math.round(speed_print * 30 / 50) +speed_layer_0 = =round(speed_print * 30 / 50) speed_topbottom = =math.ceil(speed_print * 30 / 50) speed_wall = =math.ceil(speed_print * 30 / 50) diff --git a/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg index 8604597d93..d0d4ec4d5c 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_ABS_Normal_Quality.inst.cfg @@ -16,7 +16,7 @@ material_print_temperature = 230 material_standby_temperature = 100 prime_tower_size = 16 speed_print = 55 -speed_layer_0 = =math.round(speed_print * 30 / 55) +speed_layer_0 = =round(speed_print * 30 / 55) speed_topbottom = =math.ceil(speed_print * 30 / 55) speed_wall = =math.ceil(speed_print * 30 / 55) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg index b7a9097188..6c9f031cfe 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_Draft_Print.inst.cfg @@ -15,7 +15,7 @@ material_standby_temperature = 100 prime_tower_size = 17 skin_overlap = 20 speed_print = 60 -speed_layer_0 = =math.round(speed_print * 30 / 60) +speed_layer_0 = =round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 35 / 60) speed_wall = =math.ceil(speed_print * 45 / 60) speed_wall_0 = =math.ceil(speed_wall * 35 / 45) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg index c11bbfa17d..7430c63b1b 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_Fast_Print.inst.cfg @@ -15,7 +15,7 @@ material_print_temperature = 245 material_standby_temperature = 100 prime_tower_size = 17 speed_print = 60 -speed_layer_0 = =math.round(speed_print * 30 / 60) +speed_layer_0 = =round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 30 / 60) speed_wall = =math.ceil(speed_print * 40 / 60) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg index 6526874b8e..e428c4c511 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_High_Quality.inst.cfg @@ -16,7 +16,7 @@ machine_nozzle_heat_up_speed = 1.5 material_standby_temperature = 100 prime_tower_size = 17 speed_print = 50 -speed_layer_0 = =math.round(speed_print * 30 / 50) +speed_layer_0 = =round(speed_print * 30 / 50) speed_topbottom = =math.ceil(speed_print * 30 / 50) speed_wall = =math.ceil(speed_print * 30 / 50) diff --git a/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg index 4db54e2220..a628afbe78 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_CPE_Normal_Quality.inst.cfg @@ -16,7 +16,7 @@ material_print_temperature = 240 material_standby_temperature = 100 prime_tower_size = 17 speed_print = 55 -speed_layer_0 = =math.round(speed_print * 30 / 55) +speed_layer_0 = =round(speed_print * 30 / 55) speed_topbottom = =math.ceil(speed_print * 30 / 55) speed_wall = =math.ceil(speed_print * 30 / 55) diff --git a/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg index d778d8c373..c0b28ca6b7 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PLA_Fast_Print.inst.cfg @@ -17,7 +17,7 @@ machine_nozzle_heat_up_speed = 1.6 material_standby_temperature = 100 prime_tower_enable = False speed_print = 80 -speed_layer_0 = =math.round(speed_print * 30 / 80) +speed_layer_0 = =round(speed_print * 30 / 80) speed_topbottom = =math.ceil(speed_print * 30 / 80) speed_wall = =math.ceil(speed_print * 40 / 80) speed_wall_0 = =math.ceil(speed_wall * 30 / 40) diff --git a/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg b/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg index a38df02f6e..fe05a61beb 100644 --- a/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg +++ b/resources/quality/ultimaker3/um3_aa0.4_PLA_High_Quality.inst.cfg @@ -19,7 +19,7 @@ material_print_temperature = 195 material_standby_temperature = 100 skin_overlap = 10 speed_print = 60 -speed_layer_0 = =math.round(speed_print * 30 / 60) +speed_layer_0 = =round(speed_print * 30 / 60) speed_topbottom = =math.ceil(speed_print * 30 / 60) speed_wall = =math.ceil(speed_print * 30 / 60) top_bottom_thickness = 1 diff --git a/resources/variants/ultimaker3_extended_aa04.inst.cfg b/resources/variants/ultimaker3_extended_aa04.inst.cfg index 69c800532d..6fa09c32ea 100644 --- a/resources/variants/ultimaker3_extended_aa04.inst.cfg +++ b/resources/variants/ultimaker3_extended_aa04.inst.cfg @@ -27,7 +27,7 @@ retraction_min_travel = 1.5 retraction_prime_speed = 25 skin_overlap = 15 speed_print = 70 -speed_layer_0 = =math.round(speed_print * 30 / 70) +speed_layer_0 = =round(speed_print * 30 / 70) speed_topbottom = =math.ceil(speed_print * 30 / 70) speed_wall = =math.ceil(speed_print * 30 / 70) support_angle = 60 From 79259a2717c5b3a0aa08308eedf5fc157c08edde Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 24 Nov 2016 16:00:28 +0100 Subject: [PATCH 418/438] Updated requestWrite for new API --- NetworkPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index 931d4712dc..20552a6843 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -515,7 +515,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): # This is ignored. # \param filter_by_machine Whether to filter MIME types by machine. This # is ignored. - def requestWrite(self, nodes, file_name = None, filter_by_machine = False): + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): if self._progress != 0: self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to start a new print job because the printer is busy. Please check the printer.")) self._error_message.show() From 1d2d31c81bac5f92bf811d543d9fdd4db9d2c53f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 24 Nov 2016 16:03:56 +0100 Subject: [PATCH 419/438] Update Usbprinter output request write --- plugins/USBPrinting/USBPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 13dfe967b3..7d8a11521d 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -432,7 +432,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # This is ignored. # \param filter_by_machine Whether to filter MIME types by machine. This # is ignored. - def requestWrite(self, nodes, file_name = None, filter_by_machine = False): + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): Application.getInstance().showPrintMonitor.emit(True) self.startPrint() From be2a802aa2d3b808184d57de5189f5380747e295 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 24 Nov 2016 17:05:06 +0100 Subject: [PATCH 420/438] Changed ultimaker3 def so relations are retained. --- resources/definitions/ultimaker3.def.json | 2 +- resources/variants/ultimaker3_aa04.inst.cfg | 1 - resources/variants/ultimaker3_bb04.inst.cfg | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 243cf58a8b..9465cd062a 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -128,7 +128,7 @@ "retraction_min_travel": { "value": "5" }, "retraction_prime_speed": { "value": "15" }, "skin_overlap": { "value": "10" }, - "speed_layer_0": { "value": "20" }, + "speed_layer_0": { "value": "speed_print * 30 / 70" }, "speed_prime_tower": { "value": "speed_topbottom" }, "speed_print": { "value": "35" }, "speed_support": { "value": "speed_wall_0" }, diff --git a/resources/variants/ultimaker3_aa04.inst.cfg b/resources/variants/ultimaker3_aa04.inst.cfg index 41967739b4..c8d5b8aceb 100644 --- a/resources/variants/ultimaker3_aa04.inst.cfg +++ b/resources/variants/ultimaker3_aa04.inst.cfg @@ -28,7 +28,6 @@ retraction_min_travel = =line_width * 2 retraction_prime_speed = =retraction_speed skin_overlap = 15 speed_print = 70 -speed_layer_0 = =speed_print * 30 / 70 speed_topbottom = =math.ceil(speed_print * 30 / 70) speed_wall = =math.ceil(speed_print * 30 / 70) support_angle = 60 diff --git a/resources/variants/ultimaker3_bb04.inst.cfg b/resources/variants/ultimaker3_bb04.inst.cfg index 80e6b309a4..94069901cf 100644 --- a/resources/variants/ultimaker3_bb04.inst.cfg +++ b/resources/variants/ultimaker3_bb04.inst.cfg @@ -12,6 +12,7 @@ cool_fan_speed_max = =cool_fan_speed machine_nozzle_heat_up_speed = 1.5 material_print_temperature = 215 retraction_extrusion_window = =retraction_amount +speed_layer_0 = 20 speed_wall_0 = =math.ceil(speed_wall * 25 / 30) support_bottom_height = =layer_height * 2 support_bottom_stair_step_height = =layer_height From de8e49b247c91af033ede8fa4e30fe95cf16eead Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Thu, 24 Nov 2016 17:25:30 +0100 Subject: [PATCH 421/438] JSON language fix: start_layers_at_same_position is now Start Layers With The Same Part (CURA-1112) --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index abe4bf7a3d..420cbe3659 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2327,8 +2327,8 @@ }, "start_layers_at_same_position": { - "label": "Start Layers Near Same Point", - "description": "Start printing the objects in each layer near the same point, so that we don't start a new layer with printing the piece which the previous layer ended with. This makes for better overhangs and small parts, but increases printing time.", + "label": "Start Layers With The Same Part", + "description": "In each layer start with printing the object near the same point, so that we don't start a new layer with printing the piece which the previous layer ended with. This makes for better overhangs and small parts, but increases printing time.", "type": "bool", "default_value": false, "settable_per_mesh": false, From 2df6de18142a81b6460f9afbf9c612277fc79c43 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Thu, 24 Nov 2016 17:28:34 +0100 Subject: [PATCH 422/438] JSON fix: layer_start_x and y description update for better understanding (CURA-1112) --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 420cbe3659..ba5ea5d13f 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2338,7 +2338,7 @@ "layer_start_x": { "label": "Layer Start X", - "description": "The X coordinate of the position near where to start printing objects each layer.", + "description": "The X coordinate of the position near where to find the part to start printing each layer.", "unit": "mm", "type": "float", "default_value": 0.0, @@ -2351,7 +2351,7 @@ "layer_start_y": { "label": "Layer Start Y", - "description": "The Y coordinate of the position near where to start printing objects each layer.", + "description": "The Y coordinate of the position near where to find the part to start printing each layer.", "unit": "mm", "type": "float", "default_value": 0.0, From 823993caafcb6191e313db613f1d82159d3959e1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 24 Nov 2016 17:56:17 +0100 Subject: [PATCH 423/438] ALways add prime areas CURA-2625 --- cura/BuildVolume.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 603cda14d1..e5300526d4 100644 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -442,10 +442,7 @@ class BuildVolume(SceneNode): if collision: break - - if not collision: - #Prime areas are valid. Add as normal. - result_areas[extruder_id].extend(prime_areas[extruder_id]) + result_areas[extruder_id].extend(prime_areas[extruder_id]) nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value") for area in nozzle_disallowed_areas: From 510c988c431b4f7ac6263fd49f9d359b830c78c8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 09:39:19 +0100 Subject: [PATCH 424/438] Moved some setting functions back to definitions CURA-3018 --- resources/definitions/ultimaker3.def.json | 3 --- resources/variants/ultimaker3_aa04.inst.cfg | 3 --- resources/variants/ultimaker3_bb04.inst.cfg | 3 +++ 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 9465cd062a..ae0966242d 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -109,15 +109,12 @@ "multiple_mesh_overlap": { "value": "0" }, "prime_tower_enable": { "value": "True" }, "raft_airgap": { "value": "0" }, - "raft_base_speed": { "value": "20" }, "raft_base_thickness": { "value": "0.3" }, "raft_interface_line_spacing": { "value": "0.5" }, "raft_interface_line_width": { "value": "0.5" }, - "raft_interface_speed": { "value": "20" }, "raft_interface_thickness": { "value": "0.2" }, "raft_jerk": { "value": "jerk_layer_0" }, "raft_margin": { "value": "10" }, - "raft_speed": { "value": "25" }, "raft_surface_layers": { "value": "1" }, "retraction_amount": { "value": "2" }, "retraction_count_max": { "value": "10" }, diff --git a/resources/variants/ultimaker3_aa04.inst.cfg b/resources/variants/ultimaker3_aa04.inst.cfg index c8d5b8aceb..dae256c990 100644 --- a/resources/variants/ultimaker3_aa04.inst.cfg +++ b/resources/variants/ultimaker3_aa04.inst.cfg @@ -12,15 +12,12 @@ brim_width = 7 machine_nozzle_cool_down_speed = 0.9 raft_acceleration = =acceleration_print raft_airgap = 0.3 -raft_base_speed = =0.75 * raft_speed raft_base_thickness = =resolveOrValue('layer_height_0') * 1.2 raft_interface_line_spacing = =raft_interface_line_width + 0.2 raft_interface_line_width = =line_width * 2 -raft_interface_speed = =raft_speed * 0.75 raft_interface_thickness = =layer_height * 1.5 raft_jerk = =jerk_print raft_margin = 15 -raft_speed = =speed_print / 60 * 30 raft_surface_layers = 2 retraction_amount = 6.5 retraction_count_max = 25 diff --git a/resources/variants/ultimaker3_bb04.inst.cfg b/resources/variants/ultimaker3_bb04.inst.cfg index 94069901cf..b813e8474d 100644 --- a/resources/variants/ultimaker3_bb04.inst.cfg +++ b/resources/variants/ultimaker3_bb04.inst.cfg @@ -16,6 +16,8 @@ speed_layer_0 = 20 speed_wall_0 = =math.ceil(speed_wall * 25 / 30) support_bottom_height = =layer_height * 2 support_bottom_stair_step_height = =layer_height +raft_interface_speed = 20 +raft_base_speed = 20 support_infill_rate = 25 support_interface_enable = True support_join_distance = 3 @@ -23,4 +25,5 @@ support_line_width = =round(line_width * 0.4 / 0.35, 2) support_offset = 3 support_xy_distance = =wall_line_width_0 * 3 support_xy_distance_overhang = =wall_line_width_0 / 2 +raft_speed = 25 From 6d66ea51ad90a219fc1b03419ee291daa80c79f6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 11:28:38 +0100 Subject: [PATCH 425/438] Setting the support extruder in recommended mode now correctly updates them in custom CURA-3033 --- resources/qml/Settings/SettingExtruder.qml | 33 +++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml index 82d7def5ce..cbb717ac9b 100644 --- a/resources/qml/Settings/SettingExtruder.qml +++ b/resources/qml/Settings/SettingExtruder.qml @@ -30,6 +30,7 @@ SettingItem textRole: "name" anchors.fill: parent + onCurrentIndexChanged: updateCurrentColor(); MouseArea { @@ -115,12 +116,37 @@ SettingItem propertyProvider.setPropertyValue("value", extruders_model.getItem(index).index); control.color = extruders_model.getItem(index).color; } + onModelChanged: updateCurrentIndex(); - Connections + Binding { - target: propertyProvider - onPropertiesChanged: control.updateCurrentIndex(); + target: control + property: "currentIndex" + value: + { + for(var i = 0; i < extruders_model.rowCount(); ++i) + { + if(extruders_model.getItem(i).index == propertyProvider.properties.value) + { + return i; + } + } + return -1; + } + } + + // In some cases we want to update the current color without updating the currentIndex, so it's a seperate function. + function updateCurrentColor() + { + for(var i = 0; i < extruders_model.rowCount(); ++i) + { + if(extruders_model.getItem(i).index == propertyProvider.properties.value) + { + control.color = extruders_model.getItem(i).color; + return; + } + } } function updateCurrentIndex() @@ -130,7 +156,6 @@ SettingItem if(extruders_model.getItem(i).index == propertyProvider.properties.value) { control.currentIndex = i; - control.color = extruders_model.getItem(i).color; return; } } From ef4ad8d2acada37acceed2ee6a3a15b82779b804 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 11:34:49 +0100 Subject: [PATCH 426/438] Renamed um3networkprintingplugin to um3networkprinting CURA-2862 --- .../DiscoverUM3Action.py | 0 .../DiscoverUM3Action.qml | 0 .../NetworkPrinterOutputDevice.py | 0 .../NetworkPrinterOutputDevicePlugin.py | 0 .../UM3InfoComponents.qml | 0 .../{UM3NetworkPrintingPlugin => UM3NetworkPrinting}/__init__.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename plugins/{UM3NetworkPrintingPlugin => UM3NetworkPrinting}/DiscoverUM3Action.py (100%) rename plugins/{UM3NetworkPrintingPlugin => UM3NetworkPrinting}/DiscoverUM3Action.qml (100%) rename plugins/{UM3NetworkPrintingPlugin => UM3NetworkPrinting}/NetworkPrinterOutputDevice.py (100%) rename plugins/{UM3NetworkPrintingPlugin => UM3NetworkPrinting}/NetworkPrinterOutputDevicePlugin.py (100%) rename plugins/{UM3NetworkPrintingPlugin => UM3NetworkPrinting}/UM3InfoComponents.qml (100%) rename plugins/{UM3NetworkPrintingPlugin => UM3NetworkPrinting}/__init__.py (100%) diff --git a/plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py similarity index 100% rename from plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.py rename to plugins/UM3NetworkPrinting/DiscoverUM3Action.py diff --git a/plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml similarity index 100% rename from plugins/UM3NetworkPrintingPlugin/DiscoverUM3Action.qml rename to plugins/UM3NetworkPrinting/DiscoverUM3Action.qml diff --git a/plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py similarity index 100% rename from plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevice.py rename to plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py diff --git a/plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py similarity index 100% rename from plugins/UM3NetworkPrintingPlugin/NetworkPrinterOutputDevicePlugin.py rename to plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py diff --git a/plugins/UM3NetworkPrintingPlugin/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/UM3InfoComponents.qml similarity index 100% rename from plugins/UM3NetworkPrintingPlugin/UM3InfoComponents.qml rename to plugins/UM3NetworkPrinting/UM3InfoComponents.qml diff --git a/plugins/UM3NetworkPrintingPlugin/__init__.py b/plugins/UM3NetworkPrinting/__init__.py similarity index 100% rename from plugins/UM3NetworkPrintingPlugin/__init__.py rename to plugins/UM3NetworkPrinting/__init__.py From 2835ae8b07621a18aeead5166ab8aa38ce7f4bcf Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 25 Nov 2016 10:53:41 +0100 Subject: [PATCH 427/438] Improve defaults for pre-cooling This is a profile update from the materials team. Micro-profile-update. This should make prints much better with high temperature materials such as CPE+. --- resources/definitions/fdmprinter.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index ba5ea5d13f..89730a1572 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1196,7 +1196,7 @@ "description": "The minimal temperature while heating up to the Printing Temperature at which printing can already start.", "unit": "°C", "type": "float", - "default_value": 190, + "value": "max(-273.15, material_print_temperature - 10)", "minimum_value": "-273.15", "minimum_value_warning": "material_standby_temperature", "maximum_value_warning": "material_print_temperature", @@ -1210,7 +1210,7 @@ "description": "The temperature to which to already start cooling down just before the end of printing.", "unit": "°C", "type": "float", - "default_value": 180, + "value": "max(-273.15, material_print_temperature - 15)", "minimum_value": "-273.15", "minimum_value_warning": "material_standby_temperature", "maximum_value_warning": "material_print_temperature", From dcb7e2abafc03512008fe2455e24777a1db403e9 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 25 Nov 2016 11:36:06 +0100 Subject: [PATCH 428/438] Fix capitalisation of setting names Saw a few inconsistencies. --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 89730a1572..45a1d491af 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2302,7 +2302,7 @@ }, "travel_avoid_other_parts": { - "label": "Avoid Printed Parts when Traveling", + "label": "Avoid Printed Parts When Traveling", "description": "The nozzle avoids already printed parts when traveling. This option is only available when combing is enabled.", "type": "bool", "default_value": true, @@ -2327,7 +2327,7 @@ }, "start_layers_at_same_position": { - "label": "Start Layers With The Same Part", + "label": "Start Layers with the Same Part", "description": "In each layer start with printing the object near the same point, so that we don't start a new layer with printing the piece which the previous layer ended with. This makes for better overhangs and small parts, but increases printing time.", "type": "bool", "default_value": false, @@ -2362,7 +2362,7 @@ "settable_per_meshgroup": true }, "retraction_hop_enabled": { - "label": "Z Hop when Retracted", + "label": "Z Hop When Retracted", "description": "Whenever a retraction is done, the build plate is lowered to create clearance between the nozzle and the print. It prevents the nozzle from hitting the print during travel moves, reducing the chance to knock the print from the build plate.", "type": "bool", "default_value": false, From 7a4deb725a80351cb23897a3b0b39f9f38cebde2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 11:43:56 +0100 Subject: [PATCH 429/438] Clarified switching message CURA-2898 --- cura/Settings/MachineManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index a04a7398ad..1da04f16fc 100644 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -904,7 +904,7 @@ class MachineManager(QObject): "Do you want to transfer your %d changed setting(s)/override(s) to this profile?") % num_changed_settings, catalog.i18nc( "@label", - "If you transfer your settings they will override settings in the profile."), + "If you transfer your settings they will override settings in the profile. If you don't transfer these settings, they will be lost."), details, buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, From a6ea4edfb468c3e62469e498e84a78ec9fbb0b55 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 11:51:43 +0100 Subject: [PATCH 430/438] Prime tower for UMODE won't block by default if using both extruders CURA-3032 --- resources/definitions/ultimaker_original_dual.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/ultimaker_original_dual.def.json b/resources/definitions/ultimaker_original_dual.def.json index ab2f3ddf46..d133a3853f 100644 --- a/resources/definitions/ultimaker_original_dual.def.json +++ b/resources/definitions/ultimaker_original_dual.def.json @@ -73,10 +73,10 @@ "default_value": 2 }, "prime_tower_position_x": { - "default_value": 185 + "default_value": 195 }, "prime_tower_position_y": { - "default_value": 160 + "default_value": 149 } } } From 361aee3e3cbf7c7ef9030b13900cd3b88a3a4e90 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 13:29:46 +0100 Subject: [PATCH 431/438] Resolve value is now correctly displayed in profilesPage CURA-2844 --- cura/Settings/QualitySettingsModel.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cura/Settings/QualitySettingsModel.py b/cura/Settings/QualitySettingsModel.py index 07191cd49d..fc523cf13d 100644 --- a/cura/Settings/QualitySettingsModel.py +++ b/cura/Settings/QualitySettingsModel.py @@ -178,6 +178,12 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel): profile_value_source = container.getMetaDataEntry("type") profile_value = new_value + # Global tab should use resolve (if there is one) + if not self._extruder_id: + resolve_value = global_container_stack.getProperty(definition.key, "resolve") + if resolve_value is not None and profile_value is not None: + profile_value = resolve_value + user_value = None if not self._extruder_id: user_value = global_container_stack.getTop().getProperty(definition.key, "value") From bde1e052732a55f0b72a1189864f77086f80d0b2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 14:54:32 +0100 Subject: [PATCH 432/438] Changing an instanceContainer now re-checks the error state of the stacks CURA-2861 --- cura/Settings/MachineManager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 1da04f16fc..24f3847d38 100644 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -285,6 +285,8 @@ class MachineManager(QObject): elif container_type == "quality": self.activeQualityChanged.emit() + self._updateStacksHaveErrors() + def _onPropertyChanged(self, key, property_name): if property_name == "value": # Notify UI items, such as the "changed" star in profile pull down menu. From ef7004fb72ded7a89c86f25f69fa348cf589ad3d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 16:17:46 +0100 Subject: [PATCH 433/438] User changes are deserialized by workspacereader if there is no machine conflict CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 79f2399cf7..6f52f74f26 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -210,7 +210,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not user_containers: containers_to_add.append(instance_container) else: - if self._resolve_strategies["machine"] == "override": + if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None: user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) elif self._resolve_strategies["machine"] == "new": # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. From 484c7df5bc1432136b5cda7f400b50fa1dace024 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 25 Nov 2016 16:25:24 +0100 Subject: [PATCH 434/438] Added a bunch of thread yields CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 6f52f74f26..e1f0173235 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -8,7 +8,7 @@ from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry from UM.MimeTypeDatabase import MimeTypeDatabase - +from UM.Job import Job from UM.Preferences import Preferences from .WorkspaceDialog import WorkspaceDialog @@ -70,6 +70,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if stacks[0].getContainer(index).getId() != container_id: machine_conflict = True break + Job.yieldThread() material_conflict = False xml_material_profile = self._getXmlProfileClass() @@ -82,6 +83,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): materials = self._container_registry.findInstanceContainers(id=container_id) if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict material_conflict = True + Job.yieldThread() # Check if any quality_changes instance container is in conflict. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] @@ -100,6 +102,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if quality_changes[0] != instance_container: quality_changes_conflict = True break + Job.yieldThread() try: archive.open("Cura/preferences.cfg") except KeyError: @@ -164,6 +167,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): definition_container = DefinitionContainer(container_id) definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) self._container_registry.addContainer(definition_container) + Job.yieldThread() Logger.log("d", "Workspace loading is checking materials...") material_containers = [] @@ -191,6 +195,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) containers_to_add.append(material_container) material_containers.append(material_container) + Job.yieldThread() Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace @@ -204,6 +209,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Deserialize InstanceContainer by converting read data from bytes to string instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) container_type = instance_container.getMetaDataEntry("type") + Job.yieldThread() if container_type == "user": # Check if quality changes already exists. user_containers = self._container_registry.findInstanceContainers(id=container_id) @@ -296,6 +302,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): extruder_stacks.append(stack) else: global_stack = stack + Job.yieldThread() except: Logger.log("W", "We failed to serialize the stack. Trying to clean up.") # Something went really wrong. Try to remove any data that we added. @@ -372,10 +379,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for container in global_stack.getContainers(): global_stack.containersChanged.emit(container) + Job.yieldThread() for stack in extruder_stacks: stack.setNextStack(global_stack) for container in stack.getContainers(): stack.containersChanged.emit(container) + Job.yieldThread() # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) From eced6852d58cf8e7acbf871c8d83a0fd9f844360 Mon Sep 17 00:00:00 2001 From: CeDeROM Date: Sat, 26 Nov 2016 18:56:31 +0100 Subject: [PATCH 435/438] Updated reference to an example machine profile. Signed-off-by: CeDeROM --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9357e436ff..077e6d1ab7 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Third party plugins Making profiles for other printers ---------------------------------- -There are two ways of doing it. You can either use the generator [here](http://quillford.github.io/CuraProfileMaker/) or you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json) as a template. +There are two ways of doing it. You can either use the generator [here](http://quillford.github.io/CuraProfileMaker/) or you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original.def.json) as a template. * Change the machine ID to something unique * Change the machine_name to your printer's name @@ -64,4 +64,4 @@ There are two ways of doing it. You can either use the generator [here](http://q * Set the start and end gcode in machine_start_gcode and machine_end_gcode * If your printer has a heated bed, set visible to true under material_bed_temperature -Once you are done, put the profile you have made into resources/machines, or in machines in your cura profile folder. +Once you are done, put the profile you have made into resources/definitions, or in definitions in your cura profile folder. From 554a8a2e6a802d25816d4f0746f470f060bc0c56 Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Mon, 28 Nov 2016 10:21:12 +0100 Subject: [PATCH 436/438] Remove stray __init__.py file from plugins dir Since it completely breaks plugin loading as the plugin system thinks it is a plugin it needs to load. Fixes CURA-3041 --- plugins/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 plugins/__init__.py diff --git a/plugins/__init__.py b/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From e888603dd5281042ffed0c323f09e25d807a3733 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 28 Nov 2016 14:31:28 +0100 Subject: [PATCH 437/438] Cura startup with setting errors now also results in Unable to slice. --- cura/Settings/MachineManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 24f3847d38..94766b23bd 100644 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -246,6 +246,7 @@ class MachineManager(QObject): quality = self._global_container_stack.findContainer({"type": "quality"}) quality.nameChanged.connect(self._onQualityNameChanged) + self._updateStacksHaveErrors() ## Update self._stacks_valid according to _checkStacksForErrors and emit if change. def _updateStacksHaveErrors(self): From 6b56d472e050b1650199d28b5107a4a5af41b9d0 Mon Sep 17 00:00:00 2001 From: Tim Kuipers Date: Mon, 28 Nov 2016 18:00:14 +0100 Subject: [PATCH 438/438] JSON fix: made retraction_combing not editable per mesh any more (CURA-1134) Whether combing is no_skin now actually is retrieved globally --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 45a1d491af..2d77d62670 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2297,7 +2297,7 @@ }, "default_value": "all", "resolve": "'noskin' if 'noskin' in extruderValues('retraction_combing') else ('all' if 'all' in extruderValues('retraction_combing') else 'off')", - "settable_per_mesh": true, + "settable_per_mesh": false, "settable_per_extruder": false }, "travel_avoid_other_parts":