././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734478811.0179114 nxt_python-3.5.1/.pre-commit-config.yaml0000644000000000000000000000147514730405733015156 0ustar00repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: check-merge-conflict - id: check-toml - id: check-yaml - id: end-of-file-fixer - id: fix-byte-order-marker - id: trailing-whitespace - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/psf/black rev: 23.1.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.0.1 hooks: - id: mypy pass_filenames: false - repo: local hooks: - id: pytest name: pytest language: system entry: poetry run pytest types: [python] pass_filenames: false ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/LICENSE0000644000000000000000000010575714727411320011705 0ustar00 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/README.md0000644000000000000000000000656114727411320012150 0ustar00# ![NXT-Python](./logo.svg) NXT-Python is a package for controlling a LEGO NXT robot using the Python programming language. It can communicate using either USB or Bluetooth. NXT-Python for Python 2 is no longer supported. NXT-Python repository is on [sourcehut][] with a mirror on [Github][]. [sourcehut]: https://sr.ht/~ni/nxt-python/ "NXT-Python repository on sourcehut" [Github]: https://github.com/schodet/nxt-python "NXT-Python repository on Github" ## Requirements - [Python 3.x](https://www.python.org) - USB communication: - [PyUSB](https://github.com/pyusb/pyusb) - Bluetooth communication: - [PyBluez](https://github.com/pybluez/pybluez) ## Installation Install NXT-Python with pip: python3 -m pip install --upgrade nxt-python See [installation][] instructions in the documentation for more informations. [installation]: https://ni.srht.site/nxt-python/latest/installation.html ## Next steps You can read the [documentation][], or start directly with the [tutorial][]. [documentation]: https://ni.srht.site/nxt-python/latest/ [tutorial]: https://ni.srht.site/nxt-python/latest/handbook/tutorial.html ## Upgrading your code If you used previous version of NXT-Python with Python 2, the documentation includes an [migration guide][]. [migration guide]: https://ni.srht.site/nxt-python/latest/migration.html ## Contact There is a [mailing list][] for questions. NXT-Python repository maintainer is Nicolas Schodet, since 2021-11-06. You can contact him on the mailing list. You can use the [Github issues page][] to report problems, but please use the mailing list for questions. [mailing list]: https://lists.sr.ht/~ni/nxt-python [Github issues page]: https://github.com/schodet/nxt-python/issues ## Thanks - Doug Lau for writing NXT\_Python, our starting point. - rhn for creating what would become v2, making lots of smaller changes, and reviewing tons of code. - Marcus Wanner for maintaining NXT-Python up to v2.2.2, his work has been amazing! - Elvin Luff for taking over the project after Marcus, making a lot of work for the port to Python 3. - mindsensors.com (esp. Ryan Kneip) for helping out with the code for a lot of their sensors, expanding the sensors covered by the type checking database, and providing hardware for testing. - HiTechnic for providing identification information for their sensors. I note that they have now included this information in their website. ;) - Linus Atorf, Samuel Leeman-Munk, melducky, Simon Levy, Steve Castellotti, Paulo Vieira, zonedabone, migpics, TC Wan, jerradgenson, henryacev, Paul Hollensen, and anyone else I forgot for various fixes and additions. - Goldsloth for making some useful changes and keeping the tickets moving after the migration to Github. - All our users for their interest and support! ## License NXT-Python is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7501733 nxt_python-3.5.1/contrib/60-libnxt.rules0000644000000000000000000000024614247214151015122 0ustar00# Allow access to the NXT brick. SUBSYSTEM=="usb", ACTION=="add", ATTR{idVendor}=="0694", ATTR{idProduct}=="0002", \ MODE="0660", GROUP="plugdev", TAG+="uaccess" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704409096.3199499 nxt_python-3.5.1/contrib/_incr_version0000755000000000000000000000034214545634010015106 0ustar00#!/bin/sh -eux sed -i pyproject.toml -e "s/^version = \"${1}\"/version = \"${2}\"/" sed -i docs/conf.py -e "s/^release = \"${1}\"/release = \"${2}\"/" git add pyproject.toml docs/conf.py git commit -m "Update version to ${2}" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1638907410.063749 nxt_python-3.5.1/docs/Makefile0000644000000000000000000000117214153737022013255 0ustar00# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/docs/about.rst0000644000000000000000000000202014727411320013447 0ustar00About ===== Contact ------- There is a `mailing list`_ for questions. NXT-Python repository maintainer is Nicolas Schodet, since 2021-11-06. You can contact him on the mailing list. You can use the `Github issues page`_ to report problems, but please use the mailing list for questions. .. _mailing list: https://lists.sr.ht/~ni/nxt-python .. _Github issues page: https://github.com/schodet/nxt-python/issues License ------- NXT-Python is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735655274.1831486 nxt_python-3.5.1/docs/api/backends.rst0000644000000000000000000000064414734777552014716 0ustar00Connection Backends =================== .. module:: nxt.backend Backends are used by :func:`nxt.locator.find`. You will usually not use them directly. USB --- .. automodule:: nxt.backend.usb :members: Bluetooth --------- .. automodule:: nxt.backend.bluetooth :members: Device file ----------- .. automodule:: nxt.backend.devfile :members: Socket ------ .. automodule:: nxt.backend.socket :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640641685.2549558 nxt_python-3.5.1/docs/api/brick.rst0000644000000000000000000000652314162432225014214 0ustar00Brick ===== .. automodule:: nxt.brick .. autoclass:: Brick Connection Management --------------------- .. automethod:: Brick.close You can also use the context manager interface:: with nxt.locator.find() as b: b.play_tone(440, 1000) # Here, connection is closed automatically. Brick Information ----------------- .. automethod:: Brick.get_device_info .. automethod:: Brick.get_battery_level .. automethod:: Brick.get_firmware_version .. automethod:: Brick.set_brick_name .. automethod:: Brick.keep_alive Sound ----- .. automethod:: Brick.play_tone_and_wait .. automethod:: Brick.play_sound_file .. automethod:: Brick.play_tone .. automethod:: Brick.stop_sound_playback Motors and Sensors ------------------ .. automethod:: Brick.get_motor .. automethod:: Brick.get_sensor Programs -------- .. automethod:: Brick.start_program .. automethod:: Brick.stop_program .. automethod:: Brick.get_current_program_name File System Access ------------------ Brick file system has no directory and file names are not case sensitive. .. automethod:: Brick.open_file .. automethod:: Brick.find_files .. automethod:: Brick.file_delete .. automethod:: Brick.delete_user_flash Mailboxes --------- Mailboxes can be used to exchange messages with the running program. .. automethod:: Brick.message_write .. automethod:: Brick.message_read Low Level Modules Access ------------------------ Low level modules access allows to read and write directly in modules memory. This can be used for example to take a screenshot or to debug the virtual machine. You need to look at the firmware source code for how to use it. .. automethod:: Brick.find_modules .. automethod:: Brick.read_io_map .. automethod:: Brick.write_io_map Low Level Output Ports Methods ------------------------------ These are low level methods, you can use the :mod:`nxt.motor` module for an easier interface. .. automethod:: Brick.set_output_state .. automethod:: Brick.get_output_state .. automethod:: Brick.reset_motor_position Low Level Intput Ports Methods ------------------------------ This are low level methods, you can use the :mod:`nxt.sensor` module for an easier interface. .. automethod:: Brick.set_input_mode .. automethod:: Brick.get_input_values .. automethod:: Brick.reset_input_scaled_value .. automethod:: Brick.ls_get_status .. automethod:: Brick.ls_write .. automethod:: Brick.ls_read Low Level Methods ----------------- Do not use these functions unless you know exactly what you are doing. .. automethod:: Brick.file_open_read .. automethod:: Brick.file_open_write .. automethod:: Brick.file_read .. automethod:: Brick.file_write .. automethod:: Brick.file_close .. automethod:: Brick.file_find_first .. automethod:: Brick.file_find_next .. automethod:: Brick.file_open_write_linear .. automethod:: Brick.file_open_write_data .. automethod:: Brick.file_open_append_data .. automethod:: Brick.module_find_first .. automethod:: Brick.module_find_next .. automethod:: Brick.module_close .. automethod:: Brick.poll_command_length .. automethod:: Brick.poll_command .. automethod:: Brick.boot .. automethod:: Brick.bluetooth_factory_reset ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426484.7361743 nxt_python-3.5.1/docs/api/error.rst0000644000000000000000000000006614155724665014265 0ustar00Errors ====== .. automodule:: nxt.error :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/docs/api/index.rst0000644000000000000000000000021114155725071014223 0ustar00API Reference ============= .. toctree:: :maxdepth: 2 locator brick motor sensors/index backends error motcont ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/docs/api/locator.rst0000644000000000000000000000007214155725071014564 0ustar00Locator ======= .. automodule:: nxt.locator :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426484.7361743 nxt_python-3.5.1/docs/api/motcont.rst0000644000000000000000000000016014155724665014612 0ustar00Linus Atorf's MotorControl Support ================================== .. automodule:: nxt.motcont :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735744838.3915462 nxt_python-3.5.1/docs/api/motor.rst0000644000000000000000000000072214735256506014271 0ustar00Motor ===== Controlling the motors directly is not very precise because the NXT firmware does not expose the needed method for precise control. If you need more than basic controls, :class:`nxt.motcont.MotCont` provides finer controls thanks to a program running on the NXT brick. Work in progress... The :class:`nxt.motor.Motor` class will probably be reworked in a future version. .. automodule:: nxt.motor :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735744748.0544767 nxt_python-3.5.1/docs/api/sensors/generic.rst0000644000000000000000000000017714735256354016246 0ustar00LEGO Mindstorms NXT Sensors =========================== .. automodule:: nxt.sensor.generic :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1735744617.268897 nxt_python-3.5.1/docs/api/sensors/hitechnic.rst0000644000000000000000000000017714735256151016563 0ustar00HiTechnic Sensors ================= Work in progress... .. automodule:: nxt.sensor.hitechnic :members: :undoc-members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735744748.0544767 nxt_python-3.5.1/docs/api/sensors/index.rst0000644000000000000000000000050714735256354015736 0ustar00Sensors ======= Work in progress... .. automodule:: nxt.sensor :members: .. automodule:: nxt.sensor.analog :members: :undoc-members: :show-inheritance: .. automodule:: nxt.sensor.digital :members: :undoc-members: :show-inheritance: .. toctree:: :maxdepth: 2 generic mindsensors hitechnic ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735744748.0544767 nxt_python-3.5.1/docs/api/sensors/mindsensors.rst0000644000000000000000000000023314735256354017167 0ustar00Mindsensors Sensors =================== Work in progress... .. automodule:: nxt.sensor.mindsensors :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736613831.165182 nxt_python-3.5.1/docs/commands/common_options.rst0000644000000000000000000000242414740517707017224 0ustar00Common options -------------- Those options are common to programs using NXT-Python. --backend NAME Enable given backend. Can be used several times to enable several backends. One of :mod:`~nxt.backend.usb`, :mod:`~nxt.backend.bluetooth`, :mod:`~nxt.backend.socket` or :mod:`~nxt.backend.devfile`. --config NAME Name of configuration file section to use. --config-filename PATH Path to configuration file. Can be used several times to use several configuration files. --name NAME Name of NXT brick (for example: NXT). Useful to find the right brick if several bricks are connected. --host ADDRESS Bluetooth address of the NXT brick (for example: 00:16:53:01:02:03). --server-host HOST Server address or name to connect to when using :mod:`~nxt.backend.socket` backend. --server-port PORT Server port to connect to when using :mod:`~nxt.backend.socket` backend. --filename FILENAME Device filename (for example: :file:`/dev/rfcomm0`), when using `~nxt.backend.devfile` backend. .. only:: man See :manpage:`nxt-python.conf(5)` documentation for better explanation of the options to find the NXT brick. .. only:: not man See :doc:`configuration file ` documentation for better explanation of the options to find the NXT brick. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736613831.165182 nxt_python-3.5.1/docs/commands/common_see_also.rst0000644000000000000000000000022014740517707017313 0ustar00.. only:: man See also -------- :manpage:`nxt-python.conf(5)` NXT-Python documentation ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3870773 nxt_python-3.5.1/docs/commands/index.rst0000644000000000000000000000015114740460627015261 0ustar00Commands ======== .. toctree:: :maxdepth: 1 nxt-push nxt-screenshot nxt-server nxt-test ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3870773 nxt_python-3.5.1/docs/commands/nxt-push.rst0000644000000000000000000000175414740460627015752 0ustar00Manual page for nxt-push ======================== Synopsis -------- **nxt-push** [**--backend** *NAME*] [**--config** *NAME*] [**--config-filename** *PATH*] [**--name** *NAME*] [**--host** *ADDRESS*] [**--server-host** *HOST*] [**--server-port** *PORT*] [**--filename** *FILENAME*] [**--log-level** *LEVEL*] *FILE*... Description ----------- :program:`nxt-push` uploads files to a connected NXT brick file system. The NXT brick can be connected using USB, Bluetooth or over the network. Options ------- *FILE*... Names of files to send to the NXT brick. --log-level LEVEL Set the log level. One of **DEBUG**, **INFO**, **WARNING**, **ERROR**, or **CRITICAL**. Messages whose level is below the current log level will not be displayed. .. include:: common_options.rst Example ------- ``nxt-push --host 00:16:53:01:02:03 MotorControl22.rxe`` Sends the ``MotorControl22.rxe`` file to a connected NXT using its Bluetooth address to find it. .. include:: common_see_also.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3870773 nxt_python-3.5.1/docs/commands/nxt-screenshot.rst0000644000000000000000000000216314740460627017143 0ustar00Manual page for nxt-screenshot ============================== Synopsis -------- **nxt-screenshot** [**--backend** *NAME*] [**--config** *NAME*] [**--config-filename** *PATH*] [**--name** *NAME*] [**--host** *ADDRESS*] [**--server-host** *HOST*] [**--server-port** *PORT*] [**--filename** *FILENAME*] [**--log-level** *LEVEL*] *FILE* Description ----------- :command:`nxt-screenshot` takes a capture of a connected NXT brick and write the captured image to a *FILE*. The NXT brick can be connected using USB, Bluetooth or over the network. A wide range of image formats is supported, thanks to the Python Imaging Library. Options ------- *FILE* Filename to write captured image to. --log-level LEVEL Set the log level. One of **DEBUG**, **INFO**, **WARNING**, **ERROR**, or **CRITICAL**. Messages whose level is below the current log level will not be displayed. .. include:: common_options.rst Example ------- ``nxt-screenshot --host 00:16:53:01:02:03 capture.png`` Capture screen from connected NXT using its Bluetooth address. Save the result image in ``capture.png``. .. include:: common_see_also.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3870773 nxt_python-3.5.1/docs/commands/nxt-server.rst0000644000000000000000000000236114740460627016274 0ustar00Manual page for nxt-server ========================== Synopsis -------- **nxt-server** [**--backend** *NAME*] [**--config** *NAME*] [**--config-filename** *PATH*] [**--name** *NAME*] [**--host** *ADDRESS*] [**--server-host** *HOST*] [**--server-port** *PORT*] [**--filename** *FILENAME*] [**-p|--port** *PORT*] [**--log-level** *LEVEL*] Description ----------- :command:`nxt-server` serves an interface to a connected NXT brick over the network. The NXT brick can be connected using USB, Bluetooth or over the network. Options ------- -p|--port *PORT* Set the bind port. Same value must be given to the client using **--server-port** or inside Python code. Default port is 2727. --log-level LEVEL Set the log level. One of **DEBUG**, **INFO**, **WARNING**, **ERROR**, or **CRITICAL**. Messages whose level is below the current log level will not be displayed. .. include:: common_options.rst Example ------- ``nxt-server --host 00:16:53:01:02:03`` Starting the server on a computer connected to a NXT brick, accepting connection on default port 2727. ``nxt-test --server-host 192.168.1.2`` Assuming the first computer has address 192.168.1.2, remotely connect to the server to run a test. .. include:: common_see_also.rst ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736675406.414134 nxt_python-3.5.1/docs/commands/nxt-test.rst0000644000000000000000000000303714740710116015735 0ustar00Manual page for nxt-test ======================== Synopsis -------- **nxt-test** [**--backend** *NAME*] [**--config** *NAME*] [**--config-filename** *PATH*] [**--name** *NAME*] [**--host** *ADDRESS*] [**--server-host** *HOST*] [**--server-port** *PORT*] [**--filename** *FILENAME*] [**--no-sound**] [**--log-level** *LEVEL*] Description ----------- :command:`nxt-test` tests connection with a NXT brick. It allows one to debug the NXT-Python setup. The NXT brick can be connected using USB, Bluetooth or over the network. Options ------- --no-sound Be quiet, disable the sound test. --log-level LEVEL Set the log level. One of **DEBUG**, **INFO**, **WARNING**, **ERROR**, or **CRITICAL**. Messages whose level is below the current log level will not be displayed. .. include:: common_options.rst Examples -------- Running for a NXT brick connected using USB:: $ nxt-test Finding brick... NXT brick name: NXT Host address: 00:16:53:01:02:03 Bluetooth signal strengths: (0, 0, 0, 0) Free user flash: 48480 Protocol version 1.124 Firmware version 1.29 Battery level 8433 mV Play test sound...done To report problems, please enable debug logs:: $ nxt-test --log-level DEBUG When debugging Bluetooth connection problems, try to give the address explicitly:: $ nxt-test --log-level DEBUG --host 00:16:53:01:02:03 The address can be found in the "Settings" menu, under "NXT Version" screen, it is the last line labeled "ID". Add the colon to separate each pair of digits. .. include:: common_see_also.rst ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736675522.160635 nxt_python-3.5.1/docs/conf.py0000644000000000000000000000564214740710302013114 0ustar00# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- project = "NXT-Python" copyright = "2021-2025, Nicolas Schodet" author = "Nicolas Schodet" # The full version, including alpha/beta/rc tags release = "3.5.1" version = release # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = "3.4" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", ] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build"] nitpicky = True # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" html_logo = "../logo.svg" html_favicon = "favicon.ico" html_copy_source = False # -- Options for Man pages output -------------------------------------------- man_pages_authors = [ "This program is part of NXT-Python which is currently maintained by" " Nicolas Schodet.", ] man_pages = [ ( "config", "nxt-python.conf", "NXT-Python configuration file", man_pages_authors, 5, ), ( "commands/nxt-push", "nxt-push", "Push files to a NXT brick", man_pages_authors, 1, ), ( "commands/nxt-screenshot", "nxt-screenshot", "Capture screen utility for the NXT brick", man_pages_authors, 1, ), ( "commands/nxt-server", "nxt-server", "Network server for the NXT brick", man_pages_authors, 1, ), ( "commands/nxt-test", "nxt-test", "Test the NXT-Python setup", man_pages_authors, 1, ), ] # -- Options for autodoc ----------------------------------------------------- autodoc_member_order = "bysource" autodoc_typehints = "description" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736675388.8464668 nxt_python-3.5.1/docs/config.rst0000644000000000000000000001016014740710075013612 0ustar00Configuration files =================== Description ----------- The NXT-Python configuration files allow you to define the NXT bricks you want to connect to, so that you do not need to give the needed argument for every scripts. You can place a :file:`.nxt-python.conf` in your home directory, or in the current directory. You can also explicitly give configuration file name to the invoked script or function. Format ------ The configuration format is a INI-style format. It consists of several sections introduced by the section name in square brackets on its line. Each section contains a set of key/value pairs. The key and value are separated by a equal sign ('='). Configuration may include comments which are introduced by a '#' character. When looking for a brick, you can request NXT-Python to use a specific section, or :code:`[default]` if not specified. If the section is missing, or if a value is missing, the :code:`[DEFAULT]` section (note the uppercase) is used as a fallback. The following values can be defined: backends This is the space separated list of backends to use to find and connect to the brick. When not specified, a default list of backends is used: - :mod:`~nxt.backend.devfile` if :code:`filename` is given, - :mod:`~nxt.backend.socket` if :code:`server_host` or :code:`server_port` is given, - :mod:`~nxt.backend.usb` and, - :mod:`~nxt.backend.bluetooth`. name Brick name which is used to find the brick (for example: NXT). The brick name can be configured using the NXT brick menus. host Bluetooth address which is used to find the brick (for example: 00:16:53:01:02:03). When using Bluetooth backend, this allows a direct connection without having to scan to find the brick. For other backends, it can be used to select the right brick when several bricks are found. The address can be found in the "Settings" menu, under "NXT Version" screen, it is the last line labeled "ID". Add the colon to separated each pair of digits. server_host Server address or name (example: 192.168.1.3, or localhost). This is used by the :code:`socket` backend. .. only:: man The server is provided by the :manpage:`nxt-server(1)` command. .. only:: not man The server is provided by the :doc:`nxt-server ` command. server_port Server connection port (default: 2727). This is used by the :code:`socket` backend. .. only:: man The server is provided by the :manpage:`nxt-server(1)` command. .. only:: not man The server is provided by the :doc:`nxt-server ` command. filename Device file name (default is platform specific). This is used by the :mod:`~nxt.backend.devfile` backend to locate the RFCOMM device file. .. only:: man Please see NXT-Python documentation for more details on how to use this. Other values Other values are passed as-is to backends. Example ------- Given the following configuration file: .. code:: ini [DEFAULT] # Defines a fallback for every configuration name. backends = usb [default] # My default NXT, sitting on my desk. host = 00:16:53:01:02:03 name = NXT [lab] # When working at the lab, use my second NXT. name = NXT2 [robot] # Use Bluetooth for my third NXT, which is embedded in a robot, but try USB # first as this is faster. backends = usb bluetooth host = 00:16:53:aa:bb:cc name = ROBOT When using the command line, NXT-Python will connect to my default NXT if I do not give more options:: $ nxt-test Finding brick... NXT brick name: NXT ... I can request to connect to my robot NXT brick like this:: $ nxt-test --config robot Finding brick... NXT brick name: ROBOT ... Or when using a script: .. code:: python import nxt.locator b = nxt.locator.find(config="robot") Files ----- :file:`$HOME/.nxt-python.conf` Per user configuration file. :file:`.nxt-python.conf` Configuration file in current directory. .. only:: man See also -------- :manpage:`nxt-test(1)` NXT-Python documentation ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/docs/favicon.ico0000644000000000000000000000163014727411320013732 0ustar00  ‚‰PNG  IHDR szzôIIDATX…Å—ÁoE‡¿™Ùõnl7Å8­ Ié !„HA ž‘€¿ñ !\¸Áµ„Ô ÇJí‰Kˆ#âm-R)ieª¸q-Å®k{wgçq°ëÚ)qÖNLßiGóö½ofçýö’Ël§x6vO'žQr€UÇ£DR¼¹f4X| ÇÃlVr´Ö¿Æ_ÞqóÉ® öÁMŠ}IŽî8€Ãà—ßàØÉs¨ù¤G€¶6¤ú úzÜeN+˜Ôžú¿]ÿ?›xâRÛ£·[EÒh’Û¾¦LްtㇳDÍ{D×?'èÞFÔt›¥Ä…k¨³ß’/¯Ï .&ˆîPˆþî@™ä=š}à« â&ïÞE¯ÆžÇágÔ`Zä ›&Á—ÎíäšÊ¬:Våi½ô)¦òˆ£[ê­"Âri…œJ“îüJ©zOeŠ›À)¯r–Ò鈬ãÊÕ;ü¶Y§Öhòæ©%>ûðurž¡‰Ãýó]Ö°Ó—aœÂýf—Fó؈j­ÁÕ?¶ˆ£&Nn:áOpé—-~ºu—ÖÃÝn—8Š¡ZßCZÎ~Pghuc®ß®QÝÙåø1a¥,¤Ï¼»–$¿C»îÌü´R¤6¡RT|õÁ^,VÛeA5ñâp’ƒNý¡Š¡Ï{g޳]w¬nPÔ5ð¤1˜e(¼ ÝÜüŒV||î46^!×øz ð_pÊŸ€·+ó0 L.ù HÚ} ×Á”¦ 5€–»s]ô aQ`ßêOv"¸ÿÃ@ˆ®¡%9zO:,V/ઓEF“àIç¨FŠZT^÷ö}ë²0LP& ®ÈS¿c/Ù!Lwè™ Ö¯Œ¿+Ž(|™Ð³‹«°ñ ..Y¤–ö­ó„õ‹´ŸŸàµ/PÆ.X“#(­Î`¼€üÒÓw—Z’`q8ÖÁ"…^E›é[úCô„²Ïó¡²ñôÓ7+Y=9†{¦IIê7x¨}jÏÅ¥Hg{È mÚwFi3™@ilý&OtBÉe’Ç Ù¯f‚G‚§ú¬øX|²üˆö½šø*ÂgúÜS ÙÕoÔ4°9Ó›Gc[ÿàÌ;;‰00äIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735745029.1577604 nxt_python-3.5.1/docs/handbook/index.rst0000644000000000000000000000012114735257005015240 0ustar00Handbook ======== .. toctree:: :maxdepth: 2 overview tutorial tips ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3870773 nxt_python-3.5.1/docs/handbook/overview.rst0000644000000000000000000000402414740460627016007 0ustar00Overview ======== Using NXT-Python, you can control one or several NXT bricks from your computer. The program you write is running on your computer, not on the NXT brick, so the computer must be able to communicate with the brick during the whole program lifetime. Communication is done through USB or Bluetooth. You can also relay communication over the network through another computer running the :command:`nxt-server` program which is part of NXT-Python. The Brick Object ---------------- All interaction with the NXT brick is done using the :class:`~nxt.brick.Brick` class. You will first have to find a brick using the :func:`nxt.locator.find` function which will return an instance of this class. The :class:`~nxt.brick.Brick` object have functions to access the brick capabilities. There is a function for every low level system and direct command exposed by the NXT brick. There are also higher level functions to access the brick file system for example. You could do everything just using low level functions, but to access motors and sensors, you will likely prefer to use the motors and sensors objects. Motor Control ------------- You can make an instance of the :class:`~nxt.motor.Motor` class for each connected motor. This allows to control the motors with a nicer interface than using the low level function. Keep in mind that as the program is running on your computer, there can be a delay which will reduce the motor control precision. To solve this problem, you can use the :class:`~nxt.motcont.MotCont` class which cooperates with a program running on the NXT brick to allow fine control. The situation could be improved by using an improved firmware for the NXT brick, but this is not supported yet. Sensors ------- Many different sensors can be connected to the NXT brick. This is supported in NXT-Python thanks to the :mod:`~nxt.sensor` classes hierarchy. There is a class for every LEGO official sensor, and also classes for HiTechnic and Mindsensors sensors. If your sensor is not supported, please contribute its support! ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/docs/handbook/tips.rst0000644000000000000000000000215514155725071015120 0ustar00Tips and Tricks =============== Make Bluetooth Connection Faster -------------------------------- When looking for a NXT brick, the :mod:`~nxt.backend.bluetooth` backend will first try to discover which device is available. If you know the Bluetooth address of your brick, you can skip this step. First connect to your brick to get its address:: import nxt.locator with nxt.locator.find() as b: print(b.get_device_info()[0:2]) You should see something like:: ('NXT', '00:16:53:01:02:03') Now you can use the address in your programs:: import nxt.locator with nxt.locator.find(host="00:16:53:01:02:03") as b: b.play_tone(440, 1000) Or in your configuration:: # .nxt-python.conf [default] host = 00:16:53:01:02:03 How to read inputs while controlling a motor? --------------------------------------------- When using :meth:`!nxt.motor.Motor.turn`, NXT-Python is kept active while watching the motor. If you need to read sensors at the same time, you can: - use non blocking functions, like :meth:`nxt.motor.Motor.run`, - use threads, - or use :class:`nxt.motcont.MotCont`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7501733 nxt_python-3.5.1/docs/handbook/tutorial.rst0000644000000000000000000000324114247214151015773 0ustar00Tutorial ======== First, make sure that NXT-Python is installed correctly, see :doc:`/installation`. This is not a Python tutorial, you must know how to program using Python to use NXT-Python. First step when writing a NXT-Python script is to find the brick. This is the role of the :func:`nxt.locator.find` function: .. literalinclude:: ../../examples/tutorial/find.py Once the brick is found, the :func:`nxt.locator.find` function returns an object to interact with it: the :class:`~nxt.brick.Brick` object. Here the script query device information and play a tone. Now something a little bit more interesting, plug a motor on the port A and try the following script: .. literalinclude:: ../../examples/tutorial/motor.py Try changing the parameters and see what happen. You can of course drive several motors, just use the :func:`~nxt.brick.Brick.get_motor` function for each one. You can also get information from sensors: .. literalinclude:: ../../examples/tutorial/sensor_us.py Digital sensors can be automatically detected as long as the corresponding module is loaded. In the retail set, only the ultra-sound distance sensor is digital, all the other sensors are analog. When using an analog sensor, you must give the sensor class explicitly: .. literalinclude:: ../../examples/tutorial/sensor_touch.py If you run into problems, you can increase the log level. NXT-Python is using the :mod:`logging` module from the standard Python distribution. Try this script: .. literalinclude:: ../../examples/tutorial/debug.py You should now have enough information to start playing with NXT-Python. See the :doc:`/api/index`, or the :doc:`/handbook/tips` pages for more informations. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736613831.165182 nxt_python-3.5.1/docs/index.rst0000644000000000000000000000131014740517707013460 0ustar00NXT-Python ========== NXT-Python is a package for controlling a LEGO NXT robot using the Python programming language. It can communicate using either USB or Bluetooth. NXT-Python for Python 2 is no longer supported. NXT-Python sources are available on `NXT-Python repository on sourcehut`_ and mirrored on `NXT-Python repository on Github`_. .. _NXT-Python repository on sourcehut: https://sr.ht/~ni/nxt-python/ .. _NXT-Python repository on Github: https://github.com/schodet/nxt-python .. toctree:: :maxdepth: 2 installation handbook/index api/index commands/index config migration about Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736633256.3202848 nxt_python-3.5.1/docs/installation.rst0000644000000000000000000000535714740565650015071 0ustar00Installation ============ Install NXT-Python with :command:`pip`:: python3 -m pip install --upgrade nxt-python To check that NXT-Python is correctly installed, connect your NXT brick using a USB cable, and run:: nxt-test In case of problem, enable debugging for extra diagnostics:: nxt-test --log-level=debug USB --- USB support is provided by PyUSB which is installed automatically with NXT-Python. You can also use the package from your Linux distribution if you wish, in this case, install it before installing NXT-Python. PyUSB requires libusb or OpenUSB running on your system. - For Linux users, you can install it from your package manager if not installed yet. - For MacOS users, you can use ``brew install libusb`` to install it using Homebrew. - For Windows users, libusb 1.0 DLLs are provided in the releases. Check the `PyUSB FAQ`_. .. _PyUSB FAQ: https://github.com/pyusb/pyusb/blob/master/docs/faq.rst#how-do-i-install-libusb-on-windows USB access permissions for Linux ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You need to allow connection to the NXT brick for your user. If you install any package interacting with the NXT brick from your distribution, this is usually already done for you (for example, nbc, or libnxt). In other cases, copy `contrib/60-libnxt.rules` from the NXT-Python source distribution to your `/etc/udev/rules.d` directory, then make sure that udev see the change: restart your computer or use the following commands:: sudo udevadm control --reload sudo udevadm trigger Bluetooth --------- NXT-Python is only installed with USB by default, you need to install Bluetooth support explicitly. Installation from Linux Distribution Package ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The easiest solution is to install PyBluez from your distribution. On Debian based distributions, like Ubuntu:: apt install python3-bluez On ArchLinux:: pacman -S python-pybluez On Fedora:: dnf install python3-bluez Installation Using Pip ^^^^^^^^^^^^^^^^^^^^^^ You can install PyBluez using :command:`pip`. This is easy if there is a pre-build package for your system and python version. This is much harder if this is not the case. Try this:: python3 -m pip install PyBluez On MacOS: The current PyPi version of pybluez does not work with nxt-python. You will need to a more recent version from GitHub. Download and install Xcode from App Store or Apple Developer site Then activate the Xcode installation:: xcode-select -s /Applications/Xcode.app/Contents/Developer Then install pybluez from GitHub:: pip3 install https://github.com/pybluez/pybluez/tarball/07ebef044195331a48bbb90a3acb911922048ba0 As of 06/18/2022, commit 07ebef044195331a48bbb90a3acb911922048ba0 works with nxt-python 3.1.0. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/docs/migration.rst0000644000000000000000000002260014727411320014334 0ustar00Migrating to 3.0 ================ If you have a working program using NXT-Python 2, you will have to make changes to port it to NXT-Python 3. Porting to Python 3 ------------------- First of all you need to port your code to Python 3, there is an `guide`_ in Python documentation. You do not need to worry about keeping the code compatible with Python 2 as it is no longer supported. There is also an automatic script to ease the porting, called `2to3`_. .. _guide: https://docs.python.org/3/howto/pyporting.html .. _2to3: https://docs.python.org/3/library/2to3.html One major change in Python is the distinction between text string (:class:`str`) and binary string (:class:`bytes`). This means that you need to make sure to use the right one when passing arguments to NXT-Python functions. Porting to NXT-Python 3 ----------------------- Next step is to adapt the code for NXT-Python. Major Changes ^^^^^^^^^^^^^ The :func:`!nxt.locator.find_one_brick` function has been removed and replaced with the simpler :func:`nxt.locator.find` function. Actually, the whole :mod:`nxt.locator` has been replaced. Many `debug` arguments have been removed, now NXT-Python uses the :mod:`logging` module to log messages. If you want to enable debug messages, use this code before calling any NXT-Python function:: import logging logging.basicConfig(level=logging.DEBUG) The :mod:`!nxt` and :mod:`nxt.sensor` modules no longer exports name from sub-modules. In general, NXT-Python now avoids to have two names for the same object. Output port constants are replaced by enumerations, using the :mod:`enum` module: .. py:currentmodule:: nxt.motor =============================== ============================ NXT-Python 2 NXT-Python 3 =============================== ============================ :data:`!PORT_A` :attr:`Port.A` :data:`!PORT_B` :attr:`Port.B` :data:`!PORT_C` :attr:`Port.C` :data:`!MODE_IDLE` :attr:`Mode.IDLE` :data:`!MODE_MOTOR_ON` :attr:`Mode.ON` :data:`!MODE_BRAKE` :attr:`Mode.BRAKE` :data:`!MODE_REGULATED` :attr:`Mode.REGULATED` :data:`!REGULATION_IDLE` :attr:`RegulationMode.IDLE` :data:`!REGULATION_MOTOR_SPEED` :attr:`RegulationMode.SPEED` :data:`!REGULATION_MOTOR_SYNC` :attr:`RegulationMode.SYNC` :data:`!RUN_STATE_IDLE` :attr:`RunState.IDLE` :data:`!RUN_STATE_RAMP_UP` :attr:`RunState.RAMP_UP` :data:`!RUN_STATE_RUNNING` :attr:`RunState.RUNNING` :data:`!RUN_STATE_RAMP_DOWN` :attr:`RunState.RAMP_DOWN` =============================== ============================ You can now create :class:`nxt.motor.Motor` objects using :meth:`nxt.brick.Brick.get_motor`, however direct creation still works. Input port constants are replaced by enumerations, using the :mod:`enum` module. The :mod:`!nxt.sensor.common` module has been removed, its content is directly available in :mod:`nxt.sensor`: .. py:currentmodule:: nxt.sensor =============================== ============================ NXT-Python 2 NXT-Python 3 =============================== ============================ :data:`!PORT_1` :attr:`Port.S1` :data:`!PORT_2` :attr:`Port.S2` :data:`!PORT_3` :attr:`Port.S3` :data:`!PORT_4` :attr:`Port.S4` :attr:`!Type.NO_SENSOR` :attr:`Type.NO_SENSOR` :attr:`!Type.SWITCH` :attr:`Type.SWITCH` :attr:`!Type.TEMPERATURE` :attr:`Type.TEMPERATURE` :attr:`!Type.REFLECTION` :attr:`Type.REFLECTION` :attr:`!Type.ANGLE` :attr:`Type.ANGLE` :attr:`!Type.LIGHT_ACTIVE` :attr:`Type.LIGHT_ACTIVE` :attr:`!Type.LIGHT_INACTIVE` :attr:`Type.LIGHT_INACTIVE` :attr:`!Type.SOUND_DB` :attr:`Type.SOUND_DB` :attr:`!Type.SOUND_DBA` :attr:`Type.SOUND_DBA` :attr:`!Type.CUSTOM` :attr:`Type.CUSTOM` :attr:`!Type.LOW_SPEED` :attr:`Type.LOW_SPEED` :attr:`!Type.LOW_SPEED_9V` :attr:`Type.LOW_SPEED_9V` :attr:`!Type.HIGH_SPEED` :attr:`Type.HIGH_SPEED` :attr:`!Type.COLORFULL` :attr:`Type.COLOR_FULL` :attr:`!Type.COLORRED` :attr:`Type.COLOR_RED` :attr:`!Type.COLORGREEN` :attr:`Type.COLOR_GREEN` :attr:`!Type.COLORBLUE` :attr:`Type.COLOR_BLUE` :attr:`!Type.COLORNONE` :attr:`Type.COLOR_NONE` :attr:`!Type.COLOREXIT` :attr:`Type.COLOR_EXIT` :attr:`!Mode.RAW` :attr:`Mode.RAW` :attr:`!Mode.BOOLEAN` :attr:`Mode.BOOL` :attr:`!Mode.TRANSITION_CNT` :attr:`Mode.EDGE` :attr:`!Mode.PERIOD_COUNTER` :attr:`Mode.PULSE` :attr:`!Mode.PCT_FULL_SCALE` :attr:`Mode.PERCENT` :attr:`!Mode.CELSIUS` :attr:`Mode.CELSIUS` :attr:`!Mode.FAHRENHEIT` :attr:`Mode.FAHRENHEIT` :attr:`!Mode.ANGLE_STEPS` :attr:`Mode.ROTATION` :attr:`!Mode.MASK` Removed :attr:`!Mode.MASK_SLOPE` Removed =============================== ============================ You can now create :mod:`~nxt.sensor` objects using :meth:`nxt.brick.Brick.get_sensor`, however direct creation still works. For digital sensors with identification information, this can automatically detect the sensor type as with previous version. The new `cls` argument allows creating a sensor object using another class. Text String or Binary String ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The NXT brick only understands ASCII, so this is the default encoding used in NXT-Python. From :class:`nxt.brick.Brick`: .. py:currentmodule:: nxt.brick - :meth:`~Brick.get_device_info` now returns a :class:`str` for the brick name. - :meth:`~Brick.file_write`, :meth:`~Brick.write_io_map` and :meth:`~Brick.message_write` now take :class:`bytes` instead of a :class:`str`. - :meth:`~Brick.file_read`, :meth:`~Brick.read_io_map` and :meth:`~Brick.poll_command` no longer return the read size, but the returned :class:`bytes` object is cut to the right size. - :meth:`~Brick.get_current_program_name` returns a :class:`str`. - :meth:`~Brick.file_delete` is fixed and returns a :class:`str`. - :meth:`~Brick.find_files` and :meth:`~Brick.find_modules` use :class:`str` for file and module names. File Access ^^^^^^^^^^^ File reading and writing are now implemented using classes implementing :class:`io.RawIOBase`. When using :meth:`~nxt.brick.Brick.open_file`, depending of the parameters, the raw file-like object is returned directly, or wrapped in a :class:`io.BufferedIOBase` or :class:`io.TextIOBase` object. Default access mode is now text with ASCII encoding, you need to ask explicitly for binary if needed. This means that file access should be similar to regular Python file access. Renamed ^^^^^^^ From :class:`nxt.brick.Brick`: .. py:currentmodule:: nxt.brick - :meth:`!delete` has been renamed to :meth:`~Brick.file_delete`. - Many low level file and module access methods now have a ``file_`` or ``module_`` prefix. They are however not supposed to be used directly. From :mod:`nxt.error`: .. py:currentmodule:: nxt.error - :exc:`!DirProtError` and :exc:`!SysProtError` have been renamed to :exc:`DirectProtocolError` and :exc:`SystemProtocolError`. - :exc:`!FileNotFound` has been renamed to :exc:`FileNotFoundError`. - :exc:`!ModuleNotFound` has been renamed to :exc:`ModuleNotFoundError`. - New :exc:`EmptyMailboxError` and :exc:`NoActiveProgramError` have been added as subclasses of :exc:`DirectProtocolError`. Sensors: - :class:`!nxt.sensor.generic.Color20` has been renamed to :class:`nxt.sensor.generic.Color`. Removed ^^^^^^^ Some attributes are now private (prefixed with ``_``). Support for the lightblue module has been removed. It has been integrated into `PyBluez`_. .. _PyBluez: https://github.com/pybluez/pybluez From :mod:`nxt.brick`: .. py:currentmodule:: nxt.brick - :meth:`!Brick.open_read_linear` has been removed, it has never been accessible from outside the NXT brick. - :class:`!File`, :class:`!FileReader` and :class:`!FileWriter` have been removed, use :meth:`Brick.open_file`. - :class:`!FileFinder` has been removed, use :meth:`Brick.find_files`. - :class:`!ModuleFinder` has been removed, use :meth:`Brick.find_modules`. - :attr:`!Brick.mc` has been removed, make an instance using:: mc = nxt.motcont.MotCont(the_brick) From other modules: - :meth:`!nxt.motcont.MotCont.move_to` has been removed as it is not part of `MotorControl` interface and its role was not clear. - :exc:`!nxt.motcont.MotorConError` has been removed and replaced with :exc:`nxt.error.ProtocolError`. - :exc:`!nxt.telegram.InvalidReplyError` and :exc:`!nxt.telegram.InvalidOpcodeError` have been removed and replaced with :exc:`nxt.error.ProtocolError`. Module :mod:`!nxt.utils` has been removed, use :mod:`argparse`. Other Changes ^^^^^^^^^^^^^ From :class:`nxt.brick.Brick`: .. py:currentmodule:: nxt.brick - :meth:`~Brick.get_device_info` returns a tuple for the Bluetooth signal strength values instead of a single 32 bit value. - :meth:`~Brick.find_files` and :meth:`~Brick.find_modules` return an empty iterator instead of raising an exception when no file or module is found. - :meth:`~Brick.close` now closes the connection to the NXT brick. Also :class:`Brick` now implements the context manager interface so that it can be used with the ``with`` syntax. - :meth:`~Brick.boot` now takes a argument to avoid accidental firmware erasure. Other: - :class:`nxt.motcont.MotCont` methods accept tuple as argument to control several ports. - Scripts command line interface has changed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/examples/hitechnic-superpro/example1.py0000644000000000000000000000175714727411320020412 0ustar00import nxt.locator from nxt.sensor.hitechnic import SuperPro """ Experiment 1, adapted from the SuperPro demo For this demo, attach an LED between B0 and GND, a button between 3V and A0, and a 10kOhm resistor between GND and A0. When you press the button, it will turn on the LED. """ # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) # Configure B0 as output pro.set_digital_modes_byte(0x01) while True: try: # The original demo doesn't convert to volts, but I think displaying volts # to the user is better than bits. analog_value = pro.get_analog_volts()["a0"] print(f"Analog 0: {analog_value}V") if analog_value > 3.3 / 2.0: pro.set_digital_byte(0x01) else: pro.set_digital_byte(0x00) except KeyboardInterrupt: break # When program stopped, turn off outputs pro.set_digital_byte(0x00) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/examples/hitechnic-superpro/example2.py0000644000000000000000000000230314727411320020377 0ustar00import nxt.locator from nxt.sensor.hitechnic import SuperPro """ Experiment 2, adapted from the SuperPro demo For this demo, attach LEDs between B0-B5 and GND, and a 10K between GND, 3V, and A0 (the wiper pin to A0). When you turn the dial, the voltage changes and the LED's turn on roughly indicating the voltage at A0. """ # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) # Configure B0-5 as output pro.set_digital_modes_byte(0x3F) while True: try: # The original demo doesn't convert to volts, but I think displaying volts # to the user is better than bits. analog_value = pro.get_analog_volts()["a0"] print(f"Analog 0: {analog_value}V") # Convert voltage to 6 divisions segmented = int((analog_value / 3.3) * 6) # This prevents 3.3V "exactly" from being no LED's lit. if segmented == 6: segmented = 5 # Set the corresponding bit pro.set_digital_byte(2**segmented) except KeyboardInterrupt: break # When program stopped, turn off outputs pro.set_digital_byte(0x00) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/examples/hitechnic-superpro/example3.py0000644000000000000000000000224014727411320020400 0ustar00import nxt.locator from nxt.sensor.generic import Ultrasonic from nxt.sensor.hitechnic import SuperPro """ Experiment 3, adapted from the SuperPro demo For this demo, attach LEDs between B0-B5 and GND, and connect an Ultrasonic to Sensor Port 4. When you change the distance, the LED's will roughly indicate the distance in 10cm increments. """ # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) ultrasonic = Ultrasonic(brick, nxt.sensor.Port.S4) # Configure B0-5 as output pro.set_digital_modes_byte(0x3F) while True: try: # Get distance (cm) distance_cm = ultrasonic.get_distance() print(f"Distance: {distance_cm}cm") # Convert distance to 6 divisions segmented = int(distance_cm / 10.0) # This prevents longer distances from turning off the LED if segmented > 6: segmented = 5 # Set the corresponding bit pro.set_digital_byte(2**segmented) except KeyboardInterrupt: break # When program stopped, turn off outputs pro.set_digital_byte(0x00) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/examples/hitechnic-superpro/example4.py0000644000000000000000000000204614727411320020405 0ustar00from time import sleep import nxt.locator from nxt.sensor.hitechnic import SuperPro """ Experiment 4, adapted from the SuperPro demo For this demo, attach a photo-resistor between A0 and 3V and a 4.7kOhm resistor between GND and A0. Note: In testing I used a 10kOhm since I don't have 4.7kOhm handy. It measures brightness and prints to the console. The brighter it is, the higher the voltage. In my testing, a phone flashlight puts it to 3.2V and putting my hand over it results in 0.1V, with natural light levels in my workspace being around 1.9-2.0V """ # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) while True: try: # Get brightness (measured in volts) analog_value = pro.get_analog_volts()["a0"] print(f"Analog 0: {analog_value}V") # Sleep 0.1s to allow console to be read. sleep(0.1) except KeyboardInterrupt: break # When program stopped, turn off outputs pro.set_digital_byte(0x00) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704409096.3199499 nxt_python-3.5.1/examples/hitechnic-superpro/example6.py0000644000000000000000000000317014545634010020407 0ustar00import nxt.locator from nxt.sensor.hitechnic import SuperPro """ Experiment 6, adapted from the SuperPro demo For this demo, connect LEDs between B0-B1 and GND. Connect buttons between 3V and B4-B5. Connect 10kOhm resistors between B4-B5 and GND. Since the delay in signals being sent to/from the NXT is pretty long (10+ms in testing), I just turned this into a demo that uses the input and output on the digital bus for testing with the example circuit provided. If you press the B4 button, B0 turns on, and if you press B5, B1 turns on. """ # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) # Configure B0,B1 as output pro.set_digital_modes_byte(0b0000011) pro.set_digital_byte(0x00) while True: try: digital_input = pro.get_digital() left_button = digital_input["b4"] right_button = digital_input["b5"] left_button_status = "" if left_button: left_button_status = "Pressed" else: left_button_status = "Released" right_button_status = "" if right_button: right_button_status = "Pressed" else: right_button_status = "Released" print( "Left Button: {}\nRight Button: {}\n".format( left_button_status, right_button_status ) ) pro.set_digital_byte(left_button + right_button * 2) except KeyboardInterrupt: break # When program stopped, turn off outputs pro.set_digital_byte(0x00) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/examples/hitechnic-superpro/example7.py0000644000000000000000000000205314727411320020406 0ustar00import nxt.locator from nxt.sensor.hitechnic import SuperPro """ Experiment 7, adapted from the SuperPro demo For this demo, connect a LED between B4 and GND. Connect the magnetic hall-effect sensor (A3212EUA-T) as follows: * Pin 1: 3V * Pin 2: GND * Pin 3: B0 Connect B0 to 3V with a 10kOhm resistor Note: My magnetic hall effect sensor appears to be broken, so I couldn't really test this. RIP """ # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) # Configure B4 as output pro.set_digital_modes_byte(0b00010000) pro.set_digital_byte(0x00) while True: try: # Read B0 (magnet signal is inverted, low = magnet, then write back B4 with # the value. hall_effect_sensor = pro.get_digital()["b0"] print(f"Magnet: {not hall_effect_sensor}") pro.set_digital_byte((not hall_effect_sensor) << 4) except KeyboardInterrupt: break # When program stopped, turn off outputs pro.set_digital_byte(0x00) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704409096.3199499 nxt_python-3.5.1/examples/hitechnic-superpro/example9.py0000644000000000000000000000260614545634010020415 0ustar00from time import sleep import nxt.locator from nxt.sensor.hitechnic import SuperPro """ Experiment 9, adapted from the SuperPro demo For this demo, connect a speaker between O0 and O1. """ A4_FREQ = 440 C4_FREQ = 261 DS4_FREQ = 311 E4_FREQ = 329 F4_FREQ = 349 G4_FREQ = 392 # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, C4_FREQ, 3.3) sleep(0.2) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, DS4_FREQ, 3.3) sleep(0.2) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, E4_FREQ, 3.3) sleep(0.2) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, 1, 0.0) sleep(0.2) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, F4_FREQ, 3.3) pro.analog_out_voltage(1, SuperPro.AnalogOutputMode.SQUARE, A4_FREQ, 3.3) sleep(0.2) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, E4_FREQ, 3.3) pro.analog_out_voltage(1, SuperPro.AnalogOutputMode.SQUARE, G4_FREQ, 3.3) sleep(0.2) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, C4_FREQ, 3.3) pro.analog_out_voltage(1, SuperPro.AnalogOutputMode.SQUARE, E4_FREQ, 3.3) sleep(0.2) pro.analog_out_voltage(0, SuperPro.AnalogOutputMode.SQUARE, 1, 0.0) pro.analog_out_voltage(1, SuperPro.AnalogOutputMode.SQUARE, 1, 0.0) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704409096.3199499 nxt_python-3.5.1/examples/hitechnic-superpro/hitechnic-superpro-bar-display.py0000644000000000000000000000227114545634010024707 0ustar00from time import sleep import nxt.locator from nxt.sensor.hitechnic import SuperPro """ For this demo, attach LED's to the HiTechnic SuperPro from pins B0-B7 to GND. No resistors necessary, as these pins already have 220 ohm resistors. After electrical connections are completed, connect the SuperPro to Port S1 on the NXT. Note: The original kit only comes with 6 green/red LED's, so if you're short on LED's just connect B0-B5. WARNING: If you are light sensitive, avoid this demo. It flashes the B0 LED at ~5Hz. """ # Find NXT, configure sensor with nxt.locator.find() as brick: pro = SuperPro(brick, nxt.sensor.Port.S1) # Configure digital pins as outputs. # Outputs have 220 ohm resistors in series, so directly connect LED's from pins to # GND pro.set_digital_modes_byte(0xFF) # For x in 0 to 255 (inclusive) - byte representation, Python range() is # range(inclusive, exclusive) for x in range(0, 256): pro.set_digital_byte(x) print("Outputting {0:3} ({0:<08b})".format(x)) sleep(0.1) # Output 0 to turn off all pins pro.set_digital_byte(0x00) # Put all pins back as inputs (default state) pro.set_digital_modes_byte(0x00) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654790421.7212286 nxt_python-3.5.1/examples/motcont.py0000644000000000000000000000134614250414426014541 0ustar00#!/usr/bin/env python3 """NXT-Python example to use Linus Atorf's MotorControl. You need to have MotorControl22 program installed on the brick, you can find it here: https://github.com/schodet/MotorControl Build it and install it with nxc, or upload the precompiled version (MotorControl22.rxe file). """ import time import nxt.locator import nxt.motcont import nxt.motor with nxt.locator.find() as b: mc = nxt.motcont.MotCont(b) def wait(): while not mc.is_ready(nxt.motor.Port.A) or not mc.is_ready(nxt.motor.Port.B): time.sleep(0.5) mc.start() mc.cmd((nxt.motor.Port.A, nxt.motor.Port.B), 50, 360) wait() mc.cmd((nxt.motor.Port.A, nxt.motor.Port.B), -50, 360) wait() mc.stop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735655266.0230055 nxt_python-3.5.1/examples/nxt-screen.py0000755000000000000000000001416414734777542015173 0ustar00#!/usr/bin/env python3 import sys import pygame import nxt.locator from nxt.error import DirectProtocolError """ example that reads out the memory region used for the display content ("framebuffer") of a USB-connected NXT useful for those bricks with displays that died of old age (the display is probably still fine, but the glue on the ribbon cable usually isn't) this code basically reimplements what https://bricxcc.sourceforge.net/utilities.html -> "NeXTScreen" in a less obscure programming language ;-) """ PIXEL_SIZE = 6 WINDOW_WIDTH = 100 * PIXEL_SIZE WINDOW_HEIGHT = (64 + 32) * PIXEL_SIZE # Additional 32 lines for the GUI-buttons MAIN_WINDOW_SURFACE = None FPS_CLOCK = pygame.time.Clock() def NXT_get_display_data(b): # each "module" has it's own memory region, id, ... # for t in b.find_modules(): # print(t, hex(t[1])) mod_display = 0xA0001 # display is WxH = 100x64 pixels # their memory layout is in eight lines of 100 bytes, with each byte encoding # a strip of eight vertical pixels data = b"" pixels = [""] * 64 DisplayOffsetNormal = 119 for i in range(20): data += b.read_io_map(mod_display, DisplayOffsetNormal + i * 40, 40)[1] for line in range(8): for x in range(100): b = data[line * 100 + x] for y in range(8): bitsels = format( b, "08b" ) # abuse the stringification to get 8 1's and 0's pixels[line * 8 + y] += bitsels[7 - y] return pixels def initialize_pygame(): pygame.init() main_window_size = (WINDOW_WIDTH, WINDOW_HEIGHT) global MAIN_WINDOW_SURFACE MAIN_WINDOW_SURFACE = pygame.display.set_mode(main_window_size) pygame.display.set_caption("NXT Screen") # paint the buttons... rect_return = pygame.Rect( (50 - 13) * PIXEL_SIZE, (64 + 2) * PIXEL_SIZE, 26 * PIXEL_SIZE, 16 * PIXEL_SIZE ) pygame.draw.rect(MAIN_WINDOW_SURFACE, pygame.Color("orange"), rect_return) pygame.draw.polygon( MAIN_WINDOW_SURFACE, pygame.Color("antiquewhite3"), ( ((50 - 13 - 30) * PIXEL_SIZE, (64 + 2 + 8) * PIXEL_SIZE), ((50 - 13 - 30 + 26) * PIXEL_SIZE, (64 + 2) * PIXEL_SIZE), ((50 - 13 - 30 + 26) * PIXEL_SIZE, (64 + 2 + 16) * PIXEL_SIZE), ), ) pygame.draw.polygon( MAIN_WINDOW_SURFACE, pygame.Color("antiquewhite3"), ( ((50 + 13 + 30) * PIXEL_SIZE - 1, (64 + 2 + 8) * PIXEL_SIZE), ((50 + 13 + 30 - 26) * PIXEL_SIZE - 1, (64 + 2) * PIXEL_SIZE), ((50 + 13 + 30 - 26) * PIXEL_SIZE - 1, (64 + 2 + 16) * PIXEL_SIZE), ), ) rect_exit = pygame.Rect( (50 - 13) * PIXEL_SIZE, (64 + 20) * PIXEL_SIZE, 26 * PIXEL_SIZE, 10 * PIXEL_SIZE ) pygame.draw.rect(MAIN_WINDOW_SURFACE, pygame.Color("antiquewhite4"), rect_exit) def draw(pixels): for y in range(64): for x in range(100): p = pixels[y][x] if p == "0": color = pygame.Color("grey66") else: color = pygame.Color("black") rect = pygame.Rect(x * PIXEL_SIZE, y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE) pygame.draw.rect(MAIN_WINDOW_SURFACE, color, rect) def main_loop(): b = nxt.locator.find() print("Found brick:", b.get_device_info()[0]) # And play a recognizable note. b.play_tone(440, 250) while True: for event in pygame.event.get(): # react to titlebar 'X' beeing clicked if event.type == pygame.QUIT: pygame.quit() sys.exit() # or the press of the 'Esc' key elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: pygame.quit() sys.exit() elif ( event.type == pygame.MOUSEBUTTONDOWN ): # interpret mouse-button clicks on the GUI buttons if (50 - 13) * PIXEL_SIZE <= event.pos[0] <= ( 50 - 13 + 26 ) * PIXEL_SIZE and (64 + 2) * PIXEL_SIZE <= event.pos[1] <= ( 64 + 2 + 16 ) * PIXEL_SIZE: b.write_io_map(0x40001, 35, bytearray([0x80])) b.write_io_map(0x40001, 35, bytearray([0x80])) if (50 - 13 - 30) * PIXEL_SIZE <= event.pos[0] <= ( 50 - 13 - 30 + 26 ) * PIXEL_SIZE and (64 + 2) * PIXEL_SIZE <= event.pos[1] <= ( 64 + 2 + 16 ) * PIXEL_SIZE: b.write_io_map(0x40001, 34, bytearray([0x80])) b.write_io_map(0x40001, 34, bytearray([0x80])) if (50 + 13 + 30 - 26) * PIXEL_SIZE <= event.pos[0] <= ( 50 + 13 + 30 ) * PIXEL_SIZE and (64 + 2) * PIXEL_SIZE <= event.pos[1] <= ( 64 + 2 + 16 ) * PIXEL_SIZE: b.write_io_map(0x40001, 33, bytearray([0x80])) b.write_io_map(0x40001, 33, bytearray([0x80])) if (50 - 13) * PIXEL_SIZE <= event.pos[0] <= ( 50 - 13 + 26 ) * PIXEL_SIZE and (64 + 20) * PIXEL_SIZE <= event.pos[1] <= ( 64 + 20 + 10 ) * PIXEL_SIZE: try: b.stop_program() except DirectProtocolError: b.write_io_map(0x40001, 32, bytearray([0x80])) b.write_io_map(0x40001, 32, bytearray([0x80])) # beep to let the user know that a screen refresh happended # removed beep as it annoyed me :-) # b.play_tone(440, 25) p = NXT_get_display_data(b) draw(p) pygame.display.update() # keep the refresh rate low, to not interfere with the NXP too much # since usb-commands keep the command processor busy, which might tripp # up the more timing critical bluetooth handling parts # in my case 10 fps is still OK when connecting via USB: FPS_CLOCK.tick(10) if __name__ == "__main__": initialize_pygame() main_loop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7501733 nxt_python-3.5.1/examples/sensors.py0000644000000000000000000000111514247214151014543 0ustar00#!/usr/bin/env python3 """NXT-Python example to use sensors.""" import time import nxt.locator import nxt.sensor import nxt.sensor.generic with nxt.locator.find() as b: touch = b.get_sensor(nxt.sensor.Port.S1, nxt.sensor.generic.Touch) sound = b.get_sensor(nxt.sensor.Port.S2, nxt.sensor.generic.Sound) light = b.get_sensor(nxt.sensor.Port.S3, nxt.sensor.generic.Light, False) us = b.get_sensor(nxt.sensor.Port.S4) sensors = [touch, sound, light, us] while True: samples = [s.get_sample() for s in sensors] print(samples) time.sleep(0.5) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704409096.3199499 nxt_python-3.5.1/examples/stop_turning_motor.py0000644000000000000000000000147014545634010017027 0ustar00#!/usr/bin/env python3 """NXT-Python example to use thread to stop a motor.""" import threading import time import nxt.locator import nxt.motor with nxt.locator.find() as b: # Get the motor connected to the port A. mymotor = b.get_motor(nxt.motor.Port.A) stop_motor = False # controls whether the motor should stop turning # create thread that turns the motor t = threading.Thread( target=mymotor.turn, kwargs={ "power": 50, "tacho_units": 360 * 4, "brake": True, "stop_turn": lambda: stop_motor, }, ) t.start() # stop motor after 1sec (motor would turn approximately 3sec) time.sleep(1) stop_motor = True t.join() # release motor after 1sec since brake=True time.sleep(1) mymotor.idle() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7501733 nxt_python-3.5.1/examples/tutorial/debug.py0000644000000000000000000000047614247214151016011 0ustar00#!/usr/bin/python3 """NXT-Python tutorial: increase log level.""" import logging import nxt.locator # Increase the log level, must be done before using any NXT-Python function. See logging # documentation for details. logging.basicConfig(level=logging.DEBUG) with nxt.locator.find() as b: b.play_tone(440, 250) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7501733 nxt_python-3.5.1/examples/tutorial/find.py0000644000000000000000000000042214247214151015632 0ustar00#!/usr/bin/python3 """NXT-Python tutorial: find the brick.""" import nxt.locator # Find a brick. with nxt.locator.find() as b: # Once found, print its name. print("Found brick:", b.get_device_info()[0]) # And play a recognizable note. b.play_tone(440, 250) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7501733 nxt_python-3.5.1/examples/tutorial/motor.py0000644000000000000000000000053614247214151016060 0ustar00#!/usr/bin/python3 """NXT-Python tutorial: turn a motor.""" import nxt.locator import nxt.motor with nxt.locator.find() as b: # Get the motor connected to the port A. mymotor = b.get_motor(nxt.motor.Port.A) # Full circle in one direction. mymotor.turn(25, 360) # Full circle in the opposite direction. mymotor.turn(-25, 360) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7501733 nxt_python-3.5.1/examples/tutorial/sensor_touch.py0000644000000000000000000000102414247214151017424 0ustar00#!/usr/bin/python3 """NXT-Python tutorial: use touch sensor.""" import time import nxt.locator import nxt.sensor import nxt.sensor.generic with nxt.locator.find() as b: # Get the sensor connected to port 1, not a digital sensor, must give the sensor # class. mysensor = b.get_sensor(nxt.sensor.Port.S1, nxt.sensor.generic.Touch) # Read the sensor in a loop (until interrupted). print("Use Ctrl-C to interrupt") while True: value = mysensor.get_sample() print(value) time.sleep(0.5) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1654462568.7541735 nxt_python-3.5.1/examples/tutorial/sensor_us.py0000644000000000000000000000102314247214151016730 0ustar00#!/usr/bin/python3 """NXT-Python tutorial: use ultra-sonic sensor.""" import time import nxt.locator import nxt.sensor # Need to import generic sensors for auto-detection to work. import nxt.sensor.generic with nxt.locator.find() as b: # Find the sensor connected to port 4. mysensor = b.get_sensor(nxt.sensor.Port.S4) # Read the sensor in a loop (until interrupted). print("Use Ctrl-C to interrupt") while True: distance_cm = mysensor.get_sample() print(distance_cm) time.sleep(0.5) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1638907410.063749 nxt_python-3.5.1/logo.svg0000644000000000000000000002131414153737022012346 0ustar00 image/svg+xml ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1638907410.063749 nxt_python-3.5.1/nxt/__init__.py0000644000000000000000000000116214153737022013606 0ustar00# nxt.__init__ module -- LEGO Mindstorms NXT python package # Copyright (C) 2006 Douglas P Lau # Copyright (C) 2009 Marcus Wanner # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1638907410.063749 nxt_python-3.5.1/nxt/backend/__init__.py0000644000000000000000000000110514153737022015172 0ustar00# nxt.backend.__init__ module -- Backend package # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640640738.8467605 nxt_python-3.5.1/nxt/backend/bluetooth.py0000644000000000000000000001113614162430343015441 0ustar00# nxt.backend.bluetooth module -- Bluetooth backend # Copyright (C) 2006, 2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import logging import struct import nxt.brick logger = logging.getLogger(__name__) # NXT brick RFCOMM port. PORT = 1 class BluetoothSock: """Bluetooth socket connected to a NXT brick.""" #: Block size. bsize = 118 #: Connection type, used to evaluate latency. type = "bluetooth" def __init__(self, bluetooth, host): self._bluetooth = bluetooth self._host = host self._sock = None def __str__(self): return f"Bluetooth ({self._host})" def connect(self): """Connect to NXT brick. :return: Connected brick. :rtype: Brick """ logger.info("connecting via %s", self) sock = self._bluetooth.BluetoothSocket(self._bluetooth.RFCOMM) sock.connect((self._host, PORT)) self._sock = sock return nxt.brick.Brick(self) def close(self): """Close the connection.""" if self._sock is not None: logger.info("closing %s connection", self) self._sock.close() self._sock = None def send(self, data): """Send raw data. :param bytes data: Data to send. """ data = struct.pack("= (3, 12): from collections.abc import Buffer else: from typing import Any as Buffer class RawFileReader(io.RawIOBase): """Implement RawIOBase for reading a file on the NXT brick.""" def __init__(self, brick: "Brick", name: str) -> None: self._brick = brick self._handle, self._remaining = brick.file_open_read(name) def close(self) -> None: if not self.closed: super().close() self._brick.file_close(self._handle) def readable(self) -> bool: return True def readinto(self, b: Buffer) -> int: rsize = min(self._brick._sock.bsize, self._remaining, len(b)) if rsize == 0: return 0 _, data = self._brick.file_read(self._handle, rsize) size = len(data) self._remaining -= size b[0:size] = data return size class RawFileWriter(io.RawIOBase): """Implement RawIOBase for writing a file on the NXT brick.""" def __init__(self, brick: "Brick", name: str, size: int) -> None: self._brick = brick self._handle = brick.file_open_write(name, size) self._remaining = size def close(self) -> None: if not self.closed: super().close() self._brick.file_close(self._handle) def writable(self) -> bool: return True def write(self, b: Buffer) -> int: if self.closed: raise ValueError("write to closed file") if self._remaining == 0: raise ValueError("write to a full file") wsize = min(self._brick._sock.bsize, self._remaining, len(b)) _, size = self._brick.file_write(self._handle, bytes(b[:wsize])) self._remaining -= size return size class Brick: """Object connected to a NXT brick. It provides low level access to brick commands and high level access to internal brick components (file system, modules...). It can be used to create motor or sensor objects. Create an instance with :func:`nxt.locator.find`. The :class:`Brick` object implements the context manager interface, so you can use it with the ``with`` syntax to close the connection when done with it. """ def __init__(self, sock) -> None: self._sock = sock self._lock = threading.Lock() def play_tone_and_wait(self, frequency_hz: int, duration_ms: int) -> None: """Play a tone and wait until finished. :param frequency_hz: Tone frequency in Hertz. :param duration_ms: Tone duration in milliseconds. """ self.play_tone(frequency_hz, duration_ms) time.sleep(duration_ms / 1000.0) def close(self) -> None: """Disconnect from the NXT brick.""" if self._sock is not None: self._sock.close() self._sock = None def __enter__(self) -> "Brick": return self def __exit__( self, exc_type: Optional[type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self.close() def __del__(self) -> None: self.close() def open_file( self, name: str, mode: str = "r", size: Optional[int] = None, *, buffering: int = -1, encoding: Optional[str] = None, errors: Optional[str] = None, newline: Optional[str] = None, ) -> io.IOBase: """Open a file and return a corresponding file-like object. :param name: Name of the file to open. :param mode: Specification of open mode. :param size: For writing, give the final size of the file. :param buffering: Buffering control. :param encoding: Encoding for text mode. :param errors: Encoding error handling for text mode. :param newline: Newline handling for text mode. :return: A file-like object connected to the file on the NXT brick. :raises nxt.error.FileNotFoundError: When file does not exists. :raises nxt.error.FileExistsError: When file already exists. :raises nxt.error.SystemProtocolError: When no space is available. `mode` is a string which specifies how the file should be open. You can combine several characters to build the specification: ========= ===================================== Character Meaning ========= ===================================== 'r' open for reading (default) 'w' open for writing (`size` must be given) 't' use text mode (default) 'b' use binary mode ========= ===================================== When writing a file, the NXT brick needs to know the total size when opening the file, so this must be given as parameter. Other parameters (`buffering`, `encoding`, `errors` and `newline`) have the same meaning as the standard :func:`open` function, they must be given as keyword parameters. When `encoding` is ``None`` or not given, it defaults to ``ascii`` as this is the only encoding understood by the NXT brick. """ rw = None tb = None for c in mode: if c in "rw" and rw is None: rw = c elif c in "tb" and tb is None: tb = c else: raise ValueError("invalid mode") if rw is None: raise ValueError("must give read or write mode") if tb is None: tb = "t" if tb == "b": if encoding is not None: raise ValueError("invalid encoding argument for binary mode") if errors is not None: raise ValueError("invalid errors argument for binary mode") if newline is not None: raise ValueError("invalid newline argument for binary mode") else: if buffering == 0: raise ValueError("invalid buffering argument for text mode") if encoding is None: encoding = "ascii" if buffering == -1: buffering = self._sock.bsize raw: io.RawIOBase buf: io.BufferedIOBase if rw == "r": if size is not None: raise ValueError("size given for reading") raw = RawFileReader(self, name) if buffering == 0: return raw buf = io.BufferedReader(raw, buffering) else: if size is None: raise ValueError("size not given for writing") raw = RawFileWriter(self, name, size) if buffering == 0: return raw buf = io.BufferedWriter(raw, buffering) if tb == "t": return io.TextIOWrapper( cast(IO[bytes], buf), encoding, errors, newline, buffering == 1 ) else: return buf def find_files(self, pattern: str = "*.*") -> Iterator[tuple[str, int]]: """Find all files matching a pattern. :param pattern: Pattern to match files against. :return: An iterator on all matching files, returning file name and file size as a tuple. Accepted patterns are: - ``*.*``: to match anything (default), - ``.*``: to match files with any extension, - ``*.``: to match files with given extension, - ``.``: to match using full name. """ try: handle, name, size = self.file_find_first(pattern) except nxt.error.FileNotFoundError: return None try: yield name, size while True: try: _, name, size = self.file_find_next(handle) except nxt.error.FileNotFoundError: break yield name, size finally: self.file_close(handle) def find_modules(self, pattern: str = "*.*") -> Iterator[tuple[str, int, int, int]]: """Find all modules matching a pattern. :param pattern: Pattern to match modules against, use ``*.*`` (default) to match any module. :return: An iterator on all matching modules, returning module name, identifier, size and IO map size as a tuple. """ try: handle, mname, mid, msize, miomap_size = self.module_find_first(pattern) except nxt.error.ModuleNotFoundError: return None try: yield mname, mid, msize, miomap_size while True: try: _, mname, mid, msize, miomap_size = self.module_find_next(handle) except nxt.error.ModuleNotFoundError: break yield mname, mid, msize, miomap_size finally: self.module_close(handle) def get_motor(self, port: nxt.motor.Port) -> nxt.motor.Motor: """Return a motor object connected to one of the brick output port. :param port: Output port identifier. :return: The motor object. """ return nxt.motor.Motor(self, port) def get_sensor( self, port: nxt.sensor.Port, cls: Optional[type[nxt.sensor.Sensor]] = None, *args: Any, **kwargs: Any, ) -> nxt.sensor.Sensor: """Return a sensor object connected to one of the brick input port. :param port: Input port identifier. :param cls: Sensor class, or ``None`` to autodetect. :param args: Additional constructor positional arguments when `cls` is given. :param kwargs: Additional constructor keyword arguments when `cls` is given. :return: A sensor object. :raises nxt.sensor.digital.SearchError: When sensor can not be identified. When `cls` is not given or ``None``, try to detect the sensor type and return the correct sensor object. This only works for digital sensors with identification information. For autodetection to work, the module containing the sensor class must be imported at least once. See modules in :mod:`nxt.sensor`. """ if cls is None: if args or kwargs: raise ValueError("extra arguments with autodetect") base_sensor = nxt.sensor.digital.BaseDigitalSensor( self, port, check_compatible=False ) info = base_sensor.get_sensor_info() return nxt.sensor.digital.find_class(info)( self, port, check_compatible=False ) else: return cls(self, port, *args, **kwargs) def _cmd(self, tgram: nxt.telegram.Telegram) -> nxt.telegram.Telegram: """Send a message to the NXT brick and read reply. :param tgram: Message to send. :return: Reply message after status has been checked. """ assert tgram.reply_req with self._lock: self._sock.send(tgram.to_bytes()) reply_tgram = Telegram(opcode=tgram.opcode, pkt=self._sock.recv()) reply_tgram.check_status() return reply_tgram def _cmd_noreply(self, tgram: nxt.telegram.Telegram) -> None: """Send a message to the NXT brick with no reply. :param tgram: Message to send. """ assert not tgram.reply_req with self._lock: self._sock.send(tgram.to_bytes()) def start_program(self, name: str) -> None: """Start a program on the brick. :param name: Program file name (example: ``"myprogram.rxe"``). .. warning:: When starting or stopping a program, the NXT firmware resets every sensors and motors. """ tgram = Telegram(Opcode.DIRECT_START_PROGRAM) tgram.add_filename(name) self._cmd(tgram) def stop_program(self) -> None: """Stop the running program on the brick. :raises nxt.error.NoActiveProgramError: When no program is running. .. warning:: When starting or stopping a program, the NXT firmware resets every sensors and motors. """ tgram = Telegram(Opcode.DIRECT_STOP_PROGRAM) self._cmd(tgram) def play_sound_file(self, loop: bool, name: str) -> None: """Play a sound file on the brick. :param loop: Loop mode, play continuously. :param name: Sound file name. """ tgram = Telegram(Opcode.DIRECT_PLAY_SOUND_FILE, reply_req=False) tgram.add_bool(loop) tgram.add_filename(name) self._cmd_noreply(tgram) def play_tone(self, frequency_hz: int, duration_ms: int) -> None: """Play a tone on the brick, do not wait until finished. :param frequency_hz: Tone frequency in Hertz. :param duration_ms: Tone duration in milliseconds. This function do not wait until finished, if you want to play several notes, you may need :func:`play_tone_and_wait`. """ tgram = Telegram(Opcode.DIRECT_PLAY_TONE, reply_req=False) tgram.add_u16(frequency_hz) tgram.add_u16(duration_ms) self._cmd_noreply(tgram) def set_output_state( self, port: nxt.motor.Port, power: int, mode: nxt.motor.Mode, regulation_mode: nxt.motor.RegulationMode, turn_ratio: int, run_state: nxt.motor.RunState, tacho_limit: int, ) -> None: """Set output port state on the brick. :param port: Output port identifier. :param power: Motor speed or power level (-100 to 100). :param mode: Motor power mode. :param regulation_mode: Motor regulation mode. :param turn_ratio: Turn ratio (-100 to 100). Negative value shift power to the left motor. :param run_state: Motor run state. :param tacho_limit: Number of degrees the motor should rotate relative to the current position. .. warning:: This is a low level function, prefer to use :meth:`nxt.motor.Motor`, you can get one from :meth:`get_motor`. """ tgram = Telegram(Opcode.DIRECT_SET_OUT_STATE, reply_req=False) tgram.add_u8(port.value) tgram.add_s8(power) tgram.add_u8(mode.value) tgram.add_u8(regulation_mode.value) tgram.add_s8(turn_ratio) tgram.add_u8(run_state.value) tgram.add_u32(tacho_limit) self._cmd_noreply(tgram) def set_input_mode( self, port: nxt.sensor.Port, sensor_type: nxt.sensor.Type, sensor_mode: nxt.sensor.Mode, ) -> None: """Set input port mode on the brick. :param port: Input port identifier. :param sensor_type: Sensor type. :param sensor_mode: Sensor mode. .. warning:: This is a low level function, prefer to use a :mod:`nxt.sensor` class. """ tgram = Telegram(Opcode.DIRECT_SET_IN_MODE, reply_req=False) tgram.add_u8(port.value) tgram.add_u8(sensor_type.value) tgram.add_u8(sensor_mode.value) self._cmd_noreply(tgram) def get_output_state( self, port: nxt.motor.Port ) -> tuple[ nxt.motor.Port, int, nxt.motor.Mode, nxt.motor.RegulationMode, int, nxt.motor.RunState, int, int, int, int, ]: """Get output port state from the brick. :param port: Output port identifier. :return: A tuple with `port`, `power`, `mode`, `regulation_mode`, `turn_ratio`, `run_state`, `tacho_limit`, `tacho_count`, `block_tacho_count`, and `rotation_count`. Return value details: - **port** Output port identifier. - **power** Motor speed or power level (-100 to 100). - **mode** Motor power mode. - **regulation_mode** Motor regulation mode. - **turn_ratio** Turn ratio (-100 to 100). Negative value shift power to the left motor. - **run_state** Motor run state. - **tacho_limit** Number of degrees the motor should rotate. - **block_tacho_count** Number of degrees the motor rotated relative to the "block" start. - **rotation_count** Number of degrees the motor rotated relative to the program start. .. warning:: This is a low level function, prefer to use :meth:`nxt.motor.Motor`, you can get one from :meth:`get_motor`. """ tgram = Telegram(Opcode.DIRECT_GET_OUT_STATE) tgram.add_u8(port.value) tgram = self._cmd(tgram) port = nxt.motor.Port(tgram.parse_u8()) power = tgram.parse_s8() mode = nxt.motor.Mode(tgram.parse_u8()) regulation_mode = nxt.motor.RegulationMode(tgram.parse_u8()) turn_ratio = tgram.parse_s8() run_state = nxt.motor.RunState(tgram.parse_u8()) tacho_limit = tgram.parse_u32() tacho_count = tgram.parse_s32() block_tacho_count = tgram.parse_s32() rotation_count = tgram.parse_s32() return ( port, power, mode, regulation_mode, turn_ratio, run_state, tacho_limit, tacho_count, block_tacho_count, rotation_count, ) def get_input_values( self, port: nxt.sensor.Port ) -> tuple[ nxt.sensor.Port, bool, bool, nxt.sensor.Type, nxt.sensor.Mode, int, int, int, int, ]: """Get input port values from the brick. :param port: Input port identifier. :return: A tuple with `port`, `valid`, `calibrated`, `sensor_type`, `sensor_mode`, `raw_value`, `normalized_value`, `scaled_value`, and `calibrated_value`. `rotation_count`. Return value details: - **port** Input port identifier. - **valid** ``True`` if the value is valid, else ``False``. - **calibrated** Always ``False``, there is no calibration in NXT firmware. - **sensor_type** Sensor type. - **sensor_mode** Sensor mode. - **raw_value** Raw analog to digital converter value. - **normalized_value** Normalized value. - **scaled_value** Scaled value. - **calibrated_value** Always normalized value, there is no calibration in NXT firmware. .. warning:: This is a low level function, prefer to use a :mod:`nxt.sensor` class. """ tgram = Telegram(Opcode.DIRECT_GET_IN_VALS) tgram.add_u8(port.value) tgram = self._cmd(tgram) port = nxt.sensor.Port(tgram.parse_u8()) valid = tgram.parse_bool() calibrated = tgram.parse_bool() sensor_type = nxt.sensor.Type(tgram.parse_u8()) sensor_mode = nxt.sensor.Mode(tgram.parse_u8()) raw_value = tgram.parse_u16() normalized_value = tgram.parse_u16() scaled_value = tgram.parse_s16() calibrated_value = tgram.parse_s16() return ( port, valid, calibrated, sensor_type, sensor_mode, raw_value, normalized_value, scaled_value, calibrated_value, ) def reset_input_scaled_value(self, port: nxt.sensor.Port) -> None: """Reset scaled value for an input port on the brick. :param port: Input port identifier. This can be used to reset accumulated value for some sensor modes. .. warning:: This is a low level function, prefer to use a :mod:`nxt.sensor` class. """ tgram = Telegram(Opcode.DIRECT_RESET_IN_VAL) tgram.add_u8(port.value) self._cmd(tgram) def message_write(self, inbox: int, message: bytes) -> None: """Send a message to a brick mailbox. :param inbox: Mailbox number (0 to 19). :param message: Message to send (58 bytes maximum). """ if len(message) > 58: raise ValueError("message too long") tgram = Telegram(Opcode.DIRECT_MESSAGE_WRITE) tgram.add_u8(inbox) tgram.add_u8(len(message) + 1) tgram.add_bytes(message) tgram.add_u8(0) self._cmd(tgram) def reset_motor_position(self, port: nxt.motor.Port, relative: bool) -> None: """Reset block or program motor position for a brick output port. :param port: Output port identifier. :param relative: If ``True``, reset block position, if ``False``, reset program position. .. warning:: This is a low level function, prefer to use :meth:`nxt.motor.Motor`, you can get one from :meth:`get_motor`. """ tgram = Telegram(Opcode.DIRECT_RESET_POSITION) tgram.add_u8(port.value) tgram.add_bool(relative) self._cmd(tgram) def get_battery_level(self) -> int: """Get brick battery voltage. :return: Battery voltage in millivolt. """ tgram = Telegram(Opcode.DIRECT_GET_BATT_LVL) tgram = self._cmd(tgram) millivolts = tgram.parse_u16() return millivolts def stop_sound_playback(self) -> None: """Stop currently running sound file on the brick.""" tgram = Telegram(Opcode.DIRECT_STOP_SOUND) self._cmd(tgram) def keep_alive(self) -> int: """Reset the brick standby timer. :return: Sleep timeout in milliseconds. """ tgram = Telegram(Opcode.DIRECT_KEEP_ALIVE) tgram = self._cmd(tgram) sleep_timeout = tgram.parse_u32() return sleep_timeout def ls_get_status(self, port: nxt.sensor.Port) -> int: """Get status of last low-speed transaction to a brick input port. :param port: Input port identifier. :return: Number of bytes to read as a result of the transaction. :raises nxt.error.I2CPendingError: When transaction is still in progress. :raises nxt.error.DirectProtocolError: When there is an error on the bus. .. warning:: This is a low level function, prefer to use a :mod:`nxt.sensor` class. """ tgram = Telegram(Opcode.DIRECT_LS_GET_STATUS) tgram.add_u8(port.value) tgram = self._cmd(tgram) size = tgram.parse_u8() return size def ls_write(self, port: nxt.sensor.Port, tx_data: bytes, rx_bytes: int) -> None: """Write data to a brick input port using low speed transaction. :param port: Input port identifier. :param tx_data: Data to send. :param rx_bytes: Number of bytes to receive. Function returns immediately. Transaction status can be retrieved using :meth:`ls_get_status` and result must be read using :meth:`ls_read`. .. warning:: This is a low level function, prefer to use a :mod:`nxt.sensor` class. """ tgram = Telegram(Opcode.DIRECT_LS_WRITE) tgram.add_u8(port.value) tgram.add_u8(len(tx_data)) tgram.add_u8(rx_bytes) tgram.add_bytes(tx_data) self._cmd(tgram) def ls_read(self, port: nxt.sensor.Port) -> bytes: """Read result of low speed transaction. :param port: Input port identifier. :return: Data received. :raises nxt.error.I2CPendingError: When transaction is still in progress. :raises nxt.error.DirectProtocolError: When there is an error on the bus. The :meth:`ls_write` function must be called to initiate the transaction. .. warning:: This is a low level function, prefer to use a :mod:`nxt.sensor` class. """ tgram = Telegram(Opcode.DIRECT_LS_READ) tgram.add_u8(port.value) tgram = self._cmd(tgram) size = tgram.parse_u8() rx_data = tgram.parse_bytes(size) return rx_data def get_current_program_name(self) -> str: """Return name of program currently running on the brick. :return: Program file name :raises nxt.error.NoActiveProgramError: When no program is running. """ tgram = Telegram(Opcode.DIRECT_GET_CURR_PROGRAM) tgram = self._cmd(tgram) name = tgram.parse_filename() return name def message_read( self, remote_inbox: int, local_inbox: int, remove: bool ) -> tuple[int, bytes]: """Read a message from a brick mailbox. :param remote_inbox: Mailbox number (0 to 19). :param local_inbox: Local mailbox number, not used by brick. :param remove: Whether to remove the message from the mailbox. :return: A tuple with the local mailbox number and the read message. :raises nxt.error.EmptyMailboxError: When mailbox is empty. :raises nxt.error.NoActiveProgramError: When no program is running. """ tgram = Telegram(Opcode.DIRECT_MESSAGE_READ) tgram.add_u8(remote_inbox) tgram.add_u8(local_inbox) tgram.add_bool(remove) tgram = self._cmd(tgram) local_inbox = tgram.parse_u8() size = tgram.parse_u8() message = tgram.parse_bytes(size) return local_inbox, message def file_open_read(self, name: str) -> tuple[int, int]: """Open file for reading. :param name: File name. :return: The file handle and the file size. :raises nxt.error.FileNotFoundError: When file does not exists. .. warning:: This is a low level function, prefer to use :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_OPENREAD) tgram.add_filename(name) tgram = self._cmd(tgram) handle = tgram.parse_u8() size = tgram.parse_u32() return handle, size def file_open_write(self, name: str, size: int) -> int: """Open file for writing. :param name: File name. :param size: Final file size. :return: The file handle. :raises nxt.error.FileExistsError: When file already exists. :raises nxt.error.SystemProtocolError: When no space is available. .. warning:: This is a low level function, prefer to use :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_OPENWRITE) tgram.add_filename(name) tgram.add_u32(size) tgram = self._cmd(tgram) handle = tgram.parse_u8() return handle def file_read(self, handle: int, size: int) -> tuple[int, bytes]: """Read data from open file. :param handle: Open file handle. :param size: Number of bytes to read. :return: The file handle and the read data. .. warning:: This is a low level function, prefer to use :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_READ) tgram.add_u8(handle) tgram.add_u16(size) tgram = self._cmd(tgram) handle = tgram.parse_u8() size = tgram.parse_u16() data = tgram.parse_bytes(size) return handle, data def file_write(self, handle: int, data: bytes) -> tuple[int, int]: """Write data to open file. :param handle: Open file handle. :param data: Data to write. :return: The file handle and the number of bytes written. .. warning:: This is a low level function, prefer to use :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_WRITE) tgram.add_u8(handle) tgram.add_bytes(data) tgram = self._cmd(tgram) handle = tgram.parse_u8() size = tgram.parse_u16() return handle, size def file_close(self, handle: int) -> int: """Close open file. :param handle: Open file handle. :return: The closed file handle. .. warning:: This is a low level function, prefer to use :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_CLOSE) tgram.add_u8(handle) tgram = self._cmd(tgram) handle = tgram.parse_u8() return handle def file_delete(self, name: str) -> str: """Delete a file on the brick. :param name: File name. :return: The deleted file name. :raises nxt.error.FileNotFoundError: When file does not exists. """ tgram = Telegram(Opcode.SYSTEM_DELETE) tgram.add_filename(name) tgram = self._cmd(tgram) name = tgram.parse_filename() return name def file_find_first(self, pattern: str) -> tuple[int, str, int]: """Start finding files matching a pattern. :param pattern: Pattern to match files against. :return: A handle for the search, first file found name and size. :raises nxt.error.FileNotFoundError: When no file is found. .. warning:: This is a low level function, prefer to use :meth:`find_files`. """ tgram = Telegram(Opcode.SYSTEM_FINDFIRST) tgram.add_filename(pattern) tgram = self._cmd(tgram) handle = tgram.parse_u8() name = tgram.parse_filename() size = tgram.parse_u32() return handle, name, size def file_find_next(self, handle: int) -> tuple[int, str, int]: """Continue finding files. :param handle: Handle open with :meth:`file_find_first`. :return: The handle, next file found name and size. :raises nxt.error.FileNotFoundError: When no more file is found. .. warning:: This is a low level function, prefer to use :meth:`find_files`. """ tgram = Telegram(Opcode.SYSTEM_FINDNEXT) tgram.add_u8(handle) tgram = self._cmd(tgram) handle = tgram.parse_u8() name = tgram.parse_filename() size = tgram.parse_u32() return handle, name, size def get_firmware_version(self) -> tuple[tuple[int, int], tuple[int, int]]: """Get firmware version information. :return: Protocol and firmware versions, as two tuples with major and minor for each version. """ tgram = Telegram(Opcode.SYSTEM_VERSIONS) tgram = self._cmd(tgram) prot_minor = tgram.parse_u8() prot_major = tgram.parse_u8() prot_version = (prot_major, prot_minor) fw_minor = tgram.parse_u8() fw_major = tgram.parse_u8() fw_version = (fw_major, fw_minor) return prot_version, fw_version def file_open_write_linear(self, name: str, size: int) -> int: """Open file for writing, reserve a linear space. :param name: File name. :param size: Final file size. :return: The file handle. :raises nxt.error.FileExistsError: When file already exists. :raises nxt.error.SystemProtocolError: When no space is available. Linear space is required for programs, but the brick will automatically use linear mode with :meth:`file_open_write` when extension is ``.rxe``, ``.sys`` or ``.rtm``. .. warning:: This is a low level function, prefer to use :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_OPENWRITELINEAR) tgram.add_filename(name) tgram.add_u32(size) tgram = self._cmd(tgram) handle = tgram.parse_u8() return handle def file_open_write_data(self, name: str, size: int) -> int: """Open file for writing, using data mode. :param name: File name. :param size: Maximum file size. :return: The file handle. :raises nxt.error.FileExistsError: When file already exists. :raises nxt.error.SystemProtocolError: When no space is available. A data file can be written in small chunks, and can grow later. It can be used for data logging. .. warning:: This is a low level function, however, there is no support yet for data files in :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_OPENWRITEDATA) tgram.add_filename(name) tgram.add_u32(size) tgram = self._cmd(tgram) handle = tgram.parse_u8() return handle def file_open_append_data(self, name: str) -> tuple[int, int]: """Open file for appending, using data mode. :param name: File name. :return: The file handle and available size :raises nxt.error.FileNotFoundError: When file does not exists. :raises nxt.error.SystemProtocolError: When file is full or file is not a data file. The file must be a data file. The available size is the size which has not been written to last time the file was open. .. warning:: This is a low level function, however, there is no support yet for data files in :meth:`open_file`. """ tgram = Telegram(Opcode.SYSTEM_OPENAPPENDDATA) tgram.add_filename(name) tgram = self._cmd(tgram) handle = tgram.parse_u8() available_size = tgram.parse_u32() return handle, available_size def module_find_first(self, pattern: str) -> tuple[int, str, int, int, int]: """Start finding modules matching a pattern. :param pattern: Pattern to match modules against. :return: A handle for the search, first module found name, identifier, size and IO map size. :raises nxt.error.ModuleNotFoundError: When no module is found. .. warning:: This is a low level function, prefer to use :meth:`find_modules`. """ tgram = Telegram(Opcode.SYSTEM_FINDFIRSTMODULE) tgram.add_filename(pattern) tgram = self._cmd(tgram) handle = tgram.parse_u8() name = tgram.parse_filename() mod_id = tgram.parse_u32() mod_size = tgram.parse_u32() mod_iomap_size = tgram.parse_u16() return handle, name, mod_id, mod_size, mod_iomap_size def module_find_next(self, handle: int) -> tuple[int, str, int, int, int]: """Continue finding modules. :param handle: Handle open with :meth:`module_find_first`. :return: The handle, next module found name, identifier, size and IO map size. :raises nxt.error.ModuleNotFoundError: When no more module is found. .. warning:: This is a low level function, prefer to use :meth:`find_modules`. """ tgram = Telegram(Opcode.SYSTEM_FINDNEXTMODULE) tgram.add_u8(handle) tgram = self._cmd(tgram) handle = tgram.parse_u8() name = tgram.parse_filename() mod_id = tgram.parse_u32() mod_size = tgram.parse_u32() mod_iomap_size = tgram.parse_u16() return handle, name, mod_id, mod_size, mod_iomap_size def module_close(self, handle: int) -> int: """Close module search. :param handle: Open module handle. :return: The closed module handle. .. warning:: This is a low level function, prefer to use :meth:`find_modules`. """ tgram = Telegram(Opcode.SYSTEM_CLOSEMODHANDLE) tgram.add_u8(handle) tgram = self._cmd(tgram) handle = tgram.parse_u8() return handle def read_io_map(self, mod_id: int, offset: int, size: int) -> tuple[int, bytes]: """Read module IO map on the brick. :param mod_id: Module identifier. :param offset: Offset in IO map. :param size: Number of bytes to read. :return: Module identifier and read data. Module identifier can be found using :meth:`find_modules`. You need to know the structure of the module IO map. You can find it by reading the firmware source code. """ tgram = Telegram(Opcode.SYSTEM_IOMAPREAD) tgram.add_u32(mod_id) tgram.add_u16(offset) tgram.add_u16(size) tgram = self._cmd(tgram) mod_id = tgram.parse_u32() size = tgram.parse_u16() data = tgram.parse_bytes(size) return mod_id, data def write_io_map(self, mod_id: int, offset: int, data: bytes) -> tuple[int, int]: """Write module IO map on the brick. :param mod_id: Module identifier. :param offset: Offset in IO map. :param data: Data to write. :return: Module identifier and written size. Module identifier can be found using :meth:`find_modules`. You need to know the structure of the module IO map. You can find it by reading the firmware source code. """ tgram = Telegram(Opcode.SYSTEM_IOMAPWRITE) tgram.add_u32(mod_id) tgram.add_u16(offset) tgram.add_u16(len(data)) tgram.add_bytes(data) tgram = self._cmd(tgram) mod_id = tgram.parse_u32() size = tgram.parse_u16() return mod_id, size def boot(self, *, sure: bool = False) -> bytes: """Erase NXT brick firmware and go to SAM-BA boot mode. :param sure: Set to ``True`` if you are really sure. Must be a keyword parameter. :return: Brick response, should be ``"Yes\0"``. This only works on USB connection. .. danger:: This **erases the firmware** of the brick, you need to send a new firmware to use it. Sending a firmware is not supported by NXT-Python. You can use the original LEGO software or `libnxt`_ for example. Be sure to know what you are doing. .. _libnxt: https://git.ni.fr.eu.org/libnxt.git/ """ if not sure: raise ValueError("this is dangerous, please read documentation") tgram = Telegram(Opcode.SYSTEM_BOOTCMD) tgram.add_bytes(b"Let's dance: SAMBA\0") tgram = self._cmd(tgram) resp = tgram.parse_bytes() return resp def set_brick_name(self, name: str) -> None: """Set brick name. :param name: New brick name. """ tgram = Telegram(Opcode.SYSTEM_SETBRICKNAME) tgram.add_string(15, name) self._cmd(tgram) def get_device_info(self) -> tuple[str, str, tuple[int, int, int, int], int]: """Get brick information. :return: The brick name, Bluetooth address, Bluetooth signal strengths and free user flash. Bluetooth address uses this notation: ``00:16:53:xx:xx:xx``, where `xx` is the brick unique address part (`00:16:53` is the LEGO OUI used for the NXT bricks). """ tgram = Telegram(Opcode.SYSTEM_DEVICEINFO) tgram = self._cmd(tgram) name = tgram.parse_string(15) a0 = tgram.parse_u8() a1 = tgram.parse_u8() a2 = tgram.parse_u8() a3 = tgram.parse_u8() a4 = tgram.parse_u8() a5 = tgram.parse_u8() # a6 is not used, should be zero. tgram.parse_u8() address = f"{a0:02X}:{a1:02X}:{a2:02X}:{a3:02X}:{a4:02X}:{a5:02X}" signal_strengths = ( tgram.parse_u8(), tgram.parse_u8(), tgram.parse_u8(), tgram.parse_u8(), ) user_flash = tgram.parse_u32() return name, address, signal_strengths, user_flash def delete_user_flash(self) -> None: """Erase the brick user flash.""" tgram = Telegram(Opcode.SYSTEM_DELETEUSERFLASH) self._cmd(tgram) def poll_command_length(self, buf_num: int) -> tuple[int, int]: """Get number of bytes available in brick poll buffer. :param buf_num: Buffer number, 0 for USB, 1 for high speed. :return: Buffer number and number of available bytes. """ tgram = Telegram(Opcode.SYSTEM_POLLCMDLEN) tgram.add_u8(buf_num) tgram = self._cmd(tgram) buf_num = tgram.parse_u8() size = tgram.parse_u8() return buf_num, size def poll_command(self, buf_num: int, size: int) -> tuple[int, bytes]: """Get bytes from brick poll buffer. :param buf_num: Buffer number, 0 for USB, 1 for high speed. :param size: Number of bytes to read. :return: Buffer number and read bytes. """ tgram = Telegram(Opcode.SYSTEM_POLLCMD) tgram.add_u8(buf_num) tgram.add_u8(size) tgram = self._cmd(tgram) buf_num = tgram.parse_u8() size = tgram.parse_u8() command = tgram.parse_bytes(size) return buf_num, command def bluetooth_factory_reset(self) -> None: """Reset brick Bluetooth to factory settings. This only works on USB connection. """ tgram = Telegram(Opcode.SYSTEM_BTFACTORYRESET) self._cmd(tgram) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3870773 nxt_python-3.5.1/nxt/command/__init__.py0000644000000000000000000000000014740460627015220 0ustar00././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736597911.391077 nxt_python-3.5.1/nxt/command/push.py0000644000000000000000000000411514740460627014453 0ustar00# nxt.command.push module -- Push a file to the NXT brick # Copyright (C) 2006 Douglas P Lau # Copyright (C) 2010 rhn # Copyright (C) 2025 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. """Push files to a NXT brick.""" import argparse import logging import os.path import nxt.locator from nxt.error import FileNotFoundError def write_file(b: nxt.brick.Brick, fname: str) -> None: """Write file to NXT brick from file system.""" oname = os.path.basename(fname) # Read input file. with open(fname, "rb") as f: data = f.read() # Remove existing file. try: b.file_delete(oname) print(f"Overwriting {oname} on NXT") except FileNotFoundError: pass # Write new file. print(f"Pushing {oname} ({len(data)} bytes) ...", end=" ", flush=True) with b.open_file(oname, "wb", len(data)) as w: w.write(data) print("done.") def get_parser() -> argparse.ArgumentParser: """Return argument parser.""" p = argparse.ArgumentParser(description=__doc__) nxt.locator.add_arguments(p) levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") p.add_argument("--log-level", type=str.upper, choices=levels, help="set log level") p.add_argument("file", nargs="+", help="file to transfer") return p def run() -> None: """Run command.""" options = get_parser().parse_args() if options.log_level: logging.basicConfig(level=options.log_level) print("Finding brick...") with nxt.locator.find_with_options(options) as brick: for filename in options.file: write_file(brick, filename) if __name__ == "__main__": run() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3950768 nxt_python-3.5.1/nxt/command/screenshot.py0000644000000000000000000000546714740460627015664 0ustar00# nxt.command.screenshot module -- Capture the NXT screen content # Copyright (C) 2010-2025 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. """Screen capture utility for the NXT brick.""" import argparse import logging import struct from PIL import Image import nxt.locator # Those are extracted from firmware sources. DISPLAY_MODULE_ID = 0x000A0001 DISPLAY_SCREEN_OFFSET = 119 DISPLAY_WIDTH = 100 DISPLAY_HEIGHT = 64 # Read no more than 32 bytes per request. IOM_CHUNK = 32 def get_parser() -> argparse.ArgumentParser: """Return argument parser.""" p = argparse.ArgumentParser(description=__doc__) p.add_argument("image", help="image file name to write to") nxt.locator.add_arguments(p) levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") p.add_argument("--log-level", type=str.upper, choices=levels, help="set log level") return p def screenshot(b: nxt.brick.Brick) -> Image.Image: """Take a screenshot, return a PIL image. See https://ni.fr.eu.org/lego/nxt_screenshot/ for explanations. """ # Read pixels. pixels = bytes() for i in range(0, DISPLAY_WIDTH * DISPLAY_HEIGHT // 8, IOM_CHUNK): mod_id, contents = b.read_io_map( DISPLAY_MODULE_ID, DISPLAY_SCREEN_OFFSET + i, IOM_CHUNK ) pixels += contents # Transform to a PIL format. pilpixels = [] bit = 1 linebase = 0 for y in range(0, DISPLAY_HEIGHT): # Read line by line. for x in range(0, DISPLAY_WIDTH): if pixels[linebase + x] & bit: pilpixels.append(0) else: pilpixels.append(255) bit <<= 1 # When 8 lines have been read, go on with the next byte line. if bit == (1 << 8): bit = 1 linebase += DISPLAY_WIDTH # Return a PIL image. pilbuffer = struct.pack("%dB" % DISPLAY_WIDTH * DISPLAY_HEIGHT, *pilpixels) pilimage = Image.frombuffer( "L", (DISPLAY_WIDTH, DISPLAY_HEIGHT), pilbuffer, "raw", "L", 0, 1 ) return pilimage def run() -> None: """Run command.""" options = get_parser().parse_args() if options.log_level: logging.basicConfig(level=options.log_level) print("Finding brick...") with nxt.locator.find_with_options(options) as brick: image = screenshot(brick) image.save(options.image) if __name__ == "__main__": run() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3950768 nxt_python-3.5.1/nxt/command/server.py0000644000000000000000000000552214740460627015005 0ustar00# nxt.command.server module -- Serve an interface to the NXT brick # Copyright (C) 2011 zonedabone, Marcus Wanner # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. """Network server for the NXT brick.""" import argparse import logging import socket import traceback import nxt.locator def get_parser() -> argparse.ArgumentParser: """Return argument parser.""" p = argparse.ArgumentParser(description=__doc__) p.add_argument("-p", "--port", type=int, default=2727, help="bind port") nxt.locator.add_arguments(p) levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") p.add_argument("--log-level", type=str.upper, choices=levels, help="set log level") return p def serve( brick: nxt.brick.Brick, channel: socket.socket, details: tuple[str, int] ) -> None: """Handles serving the client.""" print(f"Connection from {details[0]}.") run = True try: while run: data = channel.recv(1024) if not data: break code = data[0] if code == 0x00 or code == 0x01 or code == 0x02: brick._sock.send(data) reply = brick._sock.recv() channel.send(reply) elif code == 0x80 or code == 0x81: brick._sock.send(data) elif code == 0x98: channel.send(brick._sock.type.encode("ascii")) elif code == 0x99: run = False else: raise RuntimeError("Bad protocol") except Exception: traceback.print_exc() finally: channel.close() print("Connection closed.") def run() -> None: """Run command.""" options = get_parser().parse_args() if options.log_level: logging.basicConfig(level=options.log_level) print("Finding brick...") with nxt.locator.find_with_options(options) as brick: print(f"Brick found, starting server on port {options.port}.") print("Use Ctrl-C to interrupt.") server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("", options.port)) server.listen(1) try: # Have the server serve "forever": while True: channel, details = server.accept() serve(brick, channel, details) except KeyboardInterrupt: pass if __name__ == "__main__": run() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3950768 nxt_python-3.5.1/nxt/command/test.py0000644000000000000000000000433714740460627014461 0ustar00# nxt.command.test module -- Test command # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. """Test the NXT-Python setup.""" import argparse import logging import nxt.locator def get_parser() -> argparse.ArgumentParser: """Return argument parser.""" p = argparse.ArgumentParser(description=__doc__) p.add_argument( "--no-sound", action="store_false", dest="sound", default=True, help="disable sound test", ) nxt.locator.add_arguments(p) levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") p.add_argument("--log-level", type=str.upper, choices=levels, help="set log level") return p def run() -> None: """Run command.""" options = get_parser().parse_args() if options.log_level: logging.basicConfig(level=options.log_level) print("Finding brick...") with nxt.locator.find_with_options(options) as b: name, host, signal_strengths, user_flash = b.get_device_info() print(f"NXT brick name: {name}") print(f"Host address: {host}") print(f"Bluetooth signal strengths: {signal_strengths!r}") print(f"Free user flash: {user_flash}") prot_version, fw_version = b.get_firmware_version() print(f"Protocol version {prot_version[0]}.{prot_version[1]}") print(f"Firmware version {fw_version[0]}.{fw_version[1]}") millivolts = b.get_battery_level() print(f"Battery level {millivolts} mV") if options.sound: print("Play test sound...", end="", flush=True) b.play_tone_and_wait(300, 50) b.play_tone_and_wait(400, 50) b.play_tone_and_wait(500, 50) b.play_tone_and_wait(600, 50) print("done.") if __name__ == "__main__": run() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639090047.4847102 nxt_python-3.5.1/nxt/conftest.py0000644000000000000000000000272014154503577013704 0ustar00# conftest -- Common test fixtures for doctest # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import argparse from unittest.mock import Mock, patch import pytest @pytest.fixture(autouse=True) def mock_backends(): brick = Mock() brick.get_device_info.return_value = ("NXT", None, None, None) brick.play_tone.return_value = None backend = Mock() backend.find.return_value = [brick] with patch("nxt.locator._get_default_backends") as m: m.return_value = [backend] yield m @pytest.fixture(autouse=True) def mock_config(): with patch("nxt.locator._get_config") as m: m.return_value = None yield m @pytest.fixture(autouse=True) def mock_argparse(monkeypatch): ap = Mock() parser = Mock() ns = Mock() ns.backends = None ns.name = None ns.host = None ap.return_value = parser parser.add_argument.return_value = None parser.parse_args.return_value = ns monkeypatch.setattr(argparse, "ArgumentParser", ap) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734218447.5151248 nxt_python-3.5.1/nxt/error.py0000644000000000000000000000346114727411320013201 0ustar00# nxt.error module -- NXT brick error handling # Copyright (C) 2006, 2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. class ProtocolError(Exception): """Raised on error with the NXT brick protocol.""" pass class SystemProtocolError(ProtocolError): """Raised on error with the NXT brick protocol for a system command.""" pass class FileExistsError(SystemProtocolError): """Raised when trying to create a file which already exists.""" pass class FileNotFoundError(SystemProtocolError): """Raised when trying to access a file which does not exists.""" pass class ModuleNotFoundError(SystemProtocolError): """Raised when trying to access a module which does not exists.""" pass class DirectProtocolError(ProtocolError): """Raised on error with the NXT brick protocol for a direct command.""" pass class EmptyMailboxError(DirectProtocolError): """Raised when trying to read from empty mailbox.""" pass class I2CError(DirectProtocolError): """Raised on I2C error on a sensor port.""" pass class NoActiveProgramError(DirectProtocolError): """Raised when no program is running.""" pass class I2CPendingError(I2CError): """Raised when an I2C transaction on a sensor port is still in progress.""" pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3950768 nxt_python-3.5.1/nxt/locator.py0000644000000000000000000003071414740460627013525 0ustar00# nxt.locator module -- Locate NXT bricks # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. """ The :mod:`.locator` module allows to detect connected NXT bricks and to create corresponding :class:`~nxt.brick.Brick` objects. The :func:`find` function is your main starting point to create a NXT-Python program. If you want to make a command line tool, :func:`add_arguments` and :func:`find_with_options` will make it easy to allow choosing a brick from the command line. """ import argparse import configparser import importlib import logging import os from collections.abc import Iterable, Iterator, MutableMapping from typing import Callable, Literal, Optional, Union, overload import nxt.brick __all__ = ["find", "add_arguments", "find_with_options", "BrickNotFoundError"] logger = logging.getLogger(__name__) class BrickNotFoundError(Exception): """Exception raised when searching for a NXT brick, but no brick can be found.""" pass def _get_default_backends(**filters: Union[str, int, None]) -> list[str]: """Get default backends names. :param filters: Additional filter keywords or backends parameters, used to select additional backend based on some filter parameters. """ backends = [] if "filename" in filters: backends.append("devfile") if "server_host" in filters or "server_port" in filters: backends.append("socket") backends.extend(["usb", "bluetooth"]) return backends def _get_backends(backends: list[Union[str, object]]) -> Iterator[object]: """Get backends objects. :param backends: Specify backends to use. :return: An iterator on the backends object list. """ for backend in backends: if isinstance(backend, str): if not backend.isidentifier(): raise ValueError("invalid backend identifier") module = importlib.import_module(f"nxt.backend.{backend}") backend = module.get_backend() if backend is not None: yield backend def _get_config( config: Optional[str] = "default", config_filenames: Optional[Iterable[Union[str, bytes, os.PathLike]]] = None, ) -> Optional[MutableMapping[str, str]]: """Read configuration file and get requested configuration. :param config: Name of the configuration file section to use, or ``None`` to disable configuration reading. :param config_filenames: Configuration file paths, or ``None`` for default. :return: Configuration section or ``None``. """ if config is None: return None if config_filenames is None: config_filenames = [ ".nxt-python.conf", os.path.expanduser("~/.nxt-python.conf"), ] parser = configparser.ConfigParser() logger.debug("configuration files=%s", config_filenames) read_filenames = parser.read(config_filenames) logger.debug("configuration read from %s", read_filenames) if config not in parser: logger.debug("no section %s, using %s", config, configparser.DEFAULTSECT) return parser[configparser.DEFAULTSECT] return parser[config] @overload def find( *, find_all: Literal[False] = False, backends: Optional[Iterable[Union[str, object]]] = None, custom_match: Optional[Callable[[nxt.brick.Brick], bool]] = None, config: Optional[str] = "default", config_filenames: Optional[Iterable[Union[str, bytes, os.PathLike]]] = None, name: Optional[str] = None, host: Optional[str] = None, **filters: Union[str, int, None], ) -> nxt.brick.Brick: ... @overload def find( *, find_all: Literal[True], backends: Optional[Iterable[Union[str, object]]] = None, custom_match: Optional[Callable[[nxt.brick.Brick], bool]] = None, config: Optional[str] = "default", config_filenames: Optional[Iterable[Union[str, bytes, os.PathLike]]] = None, name: Optional[str] = None, host: Optional[str] = None, **filters: Union[str, int, None], ) -> Iterator[nxt.brick.Brick]: ... def find( *, find_all: bool = False, backends: Optional[Iterable[Union[str, object]]] = None, custom_match: Optional[Callable[[nxt.brick.Brick], bool]] = None, config: Optional[str] = "default", config_filenames: Optional[Iterable[Union[str, bytes, os.PathLike]]] = None, name: Optional[str] = None, host: Optional[str] = None, **filters: Union[str, int, None], ) -> Union[nxt.brick.Brick, Iterator[nxt.brick.Brick]]: """Find a NXT brick and return it. :param find_all: ``True`` to return an iterator over all bricks found. :param backends: Specify backends to use, use ``None`` for default. :param custom_match: Function to filter bricks found. :param config: Name of the configuration file section to use, or ``None`` to disable configuration reading. :param config_filenames: Configuration file paths, or ``None`` for default. :param name: Brick name (example: ``"NXT"``). :param host: Bluetooth address (example: ``"00:16:53:01:02:03"``). :param filters: Additional filter keywords or backends parameters. :return: The found brick, or an iterator if `find_all` is ``True`` :raises BrickNotFoundError: if no brick is found and `find_all` is ``False``. Use this function to find a NXT brick. You can pass arguments to match a specific brick, for example, this will return the brick with name "NXT": >>> import nxt.locator >>> b = nxt.locator.find(name="NXT") If there is more than one matching brick, the first one found will be returned. If no brick is found, :exc:`BrickNotFoundError` is raised. If you want to find all matching bricks, you can set the `find_all` parameter to ``True``. It will return an iterator on all found bricks. If no brick is found, an empty iterator is returned. You can also use a custom function to search for your brick: >>> def is_my_brick(brick): ... name = brick.get_device_info()[0] ... return name.startswith("NXT") ... >>> for b in nxt.locator.find(find_all=True, custom_match=is_my_brick): ... b.play_tone(440, 1000) ... The `name` and `host` parameters are passed to the backends, and are also used to filter the result, so you can use the `host` parameter even when not using Bluetooth. Extra keywords arguments are given to the backends which can use them or not. See the :mod:`nxt.backend` documentation. The `backends` parameter allows overriding the default list of backends to use. Each element of the list is a backend object or a backend name. Again, see :mod:`nxt.backend` documentation for the list of available backends. Configuration is used to load default values for `backends` parameter and selection parameters. If the `config` parameter is not ``None``, a configuration will be read from files listed by the `config_filenames` parameter, or from a default list of files. The `config` parameter corresponds to the section to use for configuration. """ config_section = _get_config(config, config_filenames) if config_section is not None: if backends is None: backends = config_section.get("backends", None) if backends is not None: backends = backends.split() if custom_match is None and name is None and host is None and not filters: name = config_section.get("name", None) host = config_section.get("host", None) for key, value in config_section.items(): if key not in ("backends", "name", "host"): filters[key] = value if backends is None: backends = _get_default_backends(**filters) def iter_bricks(): for backend in _get_backends(backends): logger.info("using backend from %s", backend.__module__) for brick in backend.find(name=name, host=host, **filters): logger.debug("found brick %s", brick) if name is not None or host is not None: bname, bhost, _, _ = brick.get_device_info() logger.debug("found brick with name=%s and host=%s", bname, bhost) if name is not None and name != bname: logger.debug("brick name mismatch, %s != %s", bname, name) brick.close() continue if host is not None and host != bhost: logger.debug("brick host mismatch, %s != %s", bhost, host) brick.close() continue if custom_match is not None and not custom_match(brick): logger.debug("brick rejected by custom_match") brick.close() continue yield brick if find_all: return iter_bricks() else: brick = next(iter_bricks(), None) if brick is None: raise BrickNotFoundError("no brick found") return brick def add_arguments(parser: argparse.ArgumentParser) -> None: """Add options to an :mod:`argparse` parser to allow configuration from the command line. :param parser: An :mod:`argparse` parser. This can be used to easily design a command line interface. Use it with :func:`find_with_options`. Example: >>> import argparse >>> import nxt.locator >>> p = argparse.ArgumentParser(description="My NXT-Python program") >>> nxt.locator.add_arguments(p) >>> p.add_argument("--hello", help="say hello (example)") >>> options = p.parse_args() >>> brick = nxt.locator.find_with_options(options) """ parser.add_argument( "--backend", dest="backends", action="append", choices=("usb", "bluetooth", "socket", "devfile"), metavar="NAME", help="enable backend, can be given several times", ) parser.add_argument( "--config", metavar="NAME", help="name of configuration file section to use" ) parser.add_argument( "--config-filename", dest="config_filenames", action="append", metavar="PATH", help="configuration life path, can be given several times", ) parser.add_argument("--name", help="NXT brick name (example: NXT)") parser.add_argument( "--host", metavar="ADDRESS", help="NXT brick Bluetooth address (example: 00:16:53:01:02:03)", ) parser.add_argument( "--server-host", metavar="HOST", help="server address or name (example: localhost)", ) parser.add_argument( "--server-port", type=int, metavar="PORT", help="server port (example: 2727)" ) parser.add_argument("--filename", help="device file name (example: /dev/rfcomm0)") @overload def find_with_options( options: argparse.Namespace, *, find_all: Literal[False] = False ) -> nxt.brick.Brick: ... @overload def find_with_options( options: argparse.Namespace, *, find_all: Literal[True] ) -> Iterator[nxt.brick.Brick]: ... def find_with_options( options: argparse.Namespace, *, find_all: bool = False ) -> Union[nxt.brick.Brick, Iterator[nxt.brick.Brick]]: """Find a NXT brick and return it, using options from command line. :param argparse.Namespace options: Options returned by :meth:`argparse.ArgumentParser.parse_args` :param bool find_all: ``True`` to return an iterator over all bricks found. :return: The found brick or ``None``, or an iterator if `find_all` is ``True``. :rtype: nxt.brick.Brick or None or Iterator[nxt.brick.Brick] This is to be used together with :func:`add_arguments`. It calls :func:`find` with options received on the command line. """ kwargs = dict() for k in ( "backends", "config", "config_filenames", "name", "host", "server_host", "server_port", "filename", ): v = getattr(options, k) if v is not None: kwargs[k] = v # Split to satisfy type checking. if find_all: return find(find_all=True, **kwargs) else: return find(find_all=False, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735744838.3915462 nxt_python-3.5.1/nxt/motcont.py0000644000000000000000000002270614735256506013552 0ustar00# nxt.motcont module -- Interface to Linus Atorf's MotorControl NXC # Copyright (C) 2011 Marcus Wanner # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import time from collections.abc import Iterable from threading import Lock from typing import Union import nxt.brick import nxt.error import nxt.motor __all__ = ["MotCont"] def _power(power: int) -> str: pw = abs(power) if power < 0: pw += 100 return f"{pw:03}" def _tacho(tacholimit: int) -> str: return f"{tacholimit:06}" class MotCont: """Interface to MotorControl. This class provides an interface to Linus Atorf's MotorControl NXC program. It is a wrapper which follows the `MotorControl documentation`_ and provides command strings and timing intervals as dictated there. To use this module, you will need to put ``MotorControl22.rxe`` on your NXT brick. It can be built using NXC and the corresponding source can be found at https://github.com/schodet/MotorControl. Use :func:`MotCont.start` to start the program. You can also start it manually my using the menu on the brick. When your script exits, it would be a good idea to do :func:`MotCont.stop`. Original `MotorControl site`_ is no longer available, but you can still find a mirror on web archive. .. _MotorControl documentation: https://github.com/schodet/MotorControl/blob/master/doc/MotorControl.md .. _MotorControl site: http://www.mindstorms.rwth-aachen.de/trac/wiki/MotorControl """ def __init__(self, brick: nxt.brick.Brick) -> None: self._brick = brick self._is_ready_lock = Lock() self._last_is_ready = time.time() - 1 self._last_cmd: dict[nxt.motor.Port, float] = {} def _interval_is_ready(self) -> None: delay = 0.010 diff = time.time() - self._last_is_ready if diff < delay: time.sleep(delay - diff) def _interval_motors(self, ports: Iterable[nxt.motor.Port]) -> None: delay = 0.015 now = time.time() diff = delay for port in ports: if port in self._last_cmd: diff = min(diff, now - self._last_cmd[port]) if diff < delay: time.sleep(delay - diff) def _record_time_motors(self, ports: Iterable[nxt.motor.Port]) -> None: now = time.time() for port in ports: self._last_cmd[port] = now def _decode_ports( self, ports: Union[nxt.motor.Port, Iterable[nxt.motor.Port]], max_ports: int ) -> tuple[frozenset[nxt.motor.Port], str]: if isinstance(ports, Iterable): ports = frozenset(ports) else: ports = frozenset((ports,)) mapping = { frozenset((nxt.motor.Port.A,)): "0", frozenset((nxt.motor.Port.B,)): "1", frozenset((nxt.motor.Port.C,)): "2", frozenset((nxt.motor.Port.A, nxt.motor.Port.B)): "3", frozenset((nxt.motor.Port.A, nxt.motor.Port.C)): "4", frozenset((nxt.motor.Port.B, nxt.motor.Port.C)): "5", frozenset((nxt.motor.Port.A, nxt.motor.Port.B, nxt.motor.Port.C)): "6", } if ports not in mapping or len(ports) > max_ports: raise ValueError("invalid combination of ports") return ports, mapping[ports] def cmd( self, ports: Union[nxt.motor.Port, Iterable[nxt.motor.Port]], power: int, tacholimit: int, speedreg: bool = True, smoothstart: bool = False, brake: bool = False, ) -> None: """Send a controlled motor command to MotorControl. :param ports: Port or ports to control, use one of the port identifiers, or an iterable returning one or two of them. :param power: Speed or power, -100 to 100. :param tacholimit: Position to drive to, 1 to 999999. :param speedreg: ``True`` to enable regulation. :param smoothstart: ``True`` to enable smooth start. :param brake: ``True`` to hold brake at end of movement. The motors on the selected ports must be idle, i.e. they can not be carrying out any other movement commands. If this should happen, the NXT will indicate this error by a signal (high and low beep) and will drop the message. To find out if a motor is ready to accept new commands, use the :meth:`is_ready` method. It is also possible to stop the motor before using :meth:`set_output_state` method. In certain situations, :meth:`set_output_state` method should be used instead. See :meth:`set_output_state` for more details. """ self._interval_is_ready() ports, strports = self._decode_ports(ports, 2) self._interval_motors(ports) mode = str(0x01 * int(brake) + 0x02 * int(speedreg) + 0x04 * int(smoothstart)) command = "1" + strports + _power(power) + _tacho(tacholimit) + mode self._brick.message_write(1, command.encode("ascii")) self._record_time_motors(ports) def reset_tacho( self, ports: Union[nxt.motor.Port, Iterable[nxt.motor.Port]] ) -> None: """Reset NXT tacho count. :param ports: Port or ports to control, use one of the port identifiers, or an iterable returning one to three of them. """ self._interval_is_ready() ports, strports = self._decode_ports(ports, 3) command = "2" + strports self._brick.message_write(1, command.encode("ascii")) self._record_time_motors(ports) def is_ready(self, port: Union[nxt.motor.Port, Iterable[nxt.motor.Port]]) -> bool: """Determine the state of a single motor. :param port: Port to control, use one of the port identifiers, or an iterable returning one of them. :return: ``True`` if the motor is ready to accept new commands. """ self._interval_is_ready() ports, strports = self._decode_ports(port, 1) with self._is_ready_lock: command = "3" + strports self._brick.message_write(1, command.encode("ascii")) time.sleep(0.015) # 10ms pause from the docs seems to not be adequate reply = self._brick.message_read(0, 1, True)[1] if chr(reply[0]) != strports: raise nxt.error.ProtocolError("wrong port returned from ISMOTORREADY") self._last_is_ready = time.time() return bool(int(chr(reply[1]))) def set_output_state( self, ports: Union[nxt.motor.Port, Iterable[nxt.motor.Port]], power: int, tacholimit: int, speedreg: bool = True, ) -> None: """Send a classic motor command to MotorControl. :param ports: Port or ports to control, use one of the port identifiers, or an iterable returning one or two of them. :param power: Speed or power, -100 to 100. :param tacholimit: Position to drive to, 1 to 999999, or 0 for unlimited. :param speedreg: ``True`` to enable regulation. This is similar to the regular :meth:`nxt.brick.Brick.set_output_state` method, but informs the MotorControl program to avoid any unwanted side effect. When to use :meth:`set_output_state` instead of :meth:`cmd`: - when tacholimit is 0 for unlimited movement, - or when the motor must coast (spin freely) after tacholimit has been reached (it will overshoot then), - or when you want to be able to change the power of a running motor at runtime. Also use this method to stop a currently running motor: - To stop and let a motor spin freely (coasting), use `power` 0 and no regulation. - To stop and hold the current position (brake), use `power` 0 with regulation. """ self._interval_is_ready() ports, strports = self._decode_ports(ports, 2) self._interval_motors(ports) command = "4" + strports + _power(power) + _tacho(tacholimit) + str(speedreg) self._brick.message_write(1, command.encode("ascii")) self._record_time_motors(ports) def start(self, version: int = 22) -> None: """Start the MotorControl program on the brick. :param version: Version to start, default to 22 (version 2.2). It needs to already be present on the brick's flash and named ``MotorControlXX.rxe``, where `XX` is the version number passed as the version argument. .. warning:: When starting or stopping a program, the NXT firmware resets every sensors and motors. """ try: self._brick.stop_program() time.sleep(1) except nxt.error.DirectProtocolError: pass self._brick.start_program("MotorControl%d.rxe" % version) time.sleep(0.1) def stop(self) -> None: """Stop the MotorControl program. All this actually does is to stop the currently running program. .. warning:: When starting or stopping a program, the NXT firmware resets every sensors and motors. """ self._brick.stop_program() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734477128.7284603 nxt_python-3.5.1/nxt/motor.py0000644000000000000000000004050114730402511013200 0ustar00# nxt.motor module -- Class to control LEGO Mindstorms NXT motors # Copyright (C) 2006 Douglas P Lau # Copyright (C) 2009 Marcus Wanner, rhn # Copyright (C) 2010 rhn # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import enum import logging import time logger = logging.getLogger(__name__) class Port(enum.Enum): """Output port identifier.""" A = 0 """Output port A.""" B = 1 """Output port B.""" C = 2 """Output port C.""" class Mode(enum.Flag): """Motor mode. These are flags that can be combined together using the `|` operator. """ IDLE = 0x00 """Keep the motor unpowered.""" ON = 0x01 """Enable motor power.""" BRAKE = 0x02 """Enable brake, motor input voltage is not left floating, must also be :attr:`ON`.""" REGULATED = 0x04 """Enable regulation, must also be :attr:`ON`.""" class RegulationMode(enum.Enum): """Motor regulation mode.""" IDLE = 0 """No regulation.""" SPEED = 1 """Speed regulation, :attr:`Mode.REGULATED` must be enabled.""" SYNC = 2 """Synchronous regulation of two motors, :attr:`Mode.REGULATED` must be enabled.""" class RunState(enum.Enum): """Motor run state.""" IDLE = 0x00 """Not running.""" RAMP_UP = 0x10 """Ramping speed up.""" RUNNING = 0x20 """Running at constant speed.""" RAMP_DOWN = 0x40 """Ramping speed down.""" LIMIT_RUN_FOREVER = 0 """No angle limit.""" class BlockedException(Exception): """Raised when a motor is not moving as expected.""" pass class OutputState: """An object holding the internal state of a motor, not including rotation counters.""" def __init__(self, values): ( self.power, self.mode, self.regulation_mode, self.turn_ratio, self.run_state, self.tacho_limit, ) = values def to_list(self): """Returns a list of properties that can be used with set_output_state.""" return [ self.power, self.mode, self.regulation_mode, self.turn_ratio, self.run_state, self.tacho_limit, ] def __str__(self): return ", ".join( [ str(self.mode), str(self.regulation_mode), str(self.turn_ratio), str(self.run_state), str(self.tacho_limit), ] ) class TachoInfo: """An object containing the information about the rotation of a motor.""" def __init__(self, values): self.tacho_count, self.block_tacho_count, self.rotation_count = values def get_target(self, tacho_limit, direction): """Returns a TachoInfo object which corresponds to tacho state after moving for tacho_limit ticks in the given direction. :param int tacho_limit: Move angle. :param int direction: 1 (add) or -1 (subtract). :return: Updated state. :rtype: TachoInfo """ # TODO: adjust other fields if abs(direction) != 1: raise ValueError("invalid direction") new_tacho = self.tacho_count + direction * tacho_limit return TachoInfo([new_tacho, None, None]) def is_greater(self, target, direction): return direction * (self.tacho_count - target.tacho_count) > 0 def is_near(self, target, threshold): difference = abs(target.tacho_count - self.tacho_count) return difference < threshold def __str__(self): return str((self.tacho_count, self.block_tacho_count, self.rotation_count)) class SynchronizedTacho: def __init__(self, leader_tacho, follower_tacho): self.leader_tacho = leader_tacho self.follower_tacho = follower_tacho def get_target(self, tacho_limit, direction): """This method will leave follower's target as None""" leader_tacho = self.leader_tacho.get_target(tacho_limit, direction) return SynchronizedTacho(leader_tacho, None) def is_greater(self, other, direction): return self.leader_tacho.is_greater(other.leader_tacho, direction) def is_near(self, other, threshold): return self.leader_tacho.is_near(other.leader_tacho, threshold) def __str__(self): if self.follower_tacho is not None: t2 = str(self.follower_tacho.tacho_count) else: t2 = "None" t1 = str(self.leader_tacho.tacho_count) return f"tacho: {t1} {t2}" def get_tacho_and_state(values): """A convenience function. values is the list of values from get_output_state. Returns both OutputState and TachoInfo. """ return OutputState(values[1:7]), TachoInfo(values[7:]) class BaseMotor: """Base class for motors""" def turn( self, power, tacho_units, brake=True, timeout=1, emulate=True, stop_turn=lambda: False, ): """Use this to turn a motor. :param int power: Value between -127 and 128 (an absolute value greater than 64 is recommended) :param int tacho_units: Number of degrees to turn the motor. Values smaller than 50 are not recommended and may have strange results. :param bool brake: Whether or not to hold the motor after the function exits (either by reaching the distance or throwing an exception). :param int timeout: Number of seconds after which a BlockedException is raised if the motor doesn't turn. :param bool emulate: If set to ``False``, the motor is aware of the tacho limit. If ``True``, a run() function equivalent is used. Warning: motors remember their positions and not using emulate may lead to strange behavior, especially with synced motors :param lambda: bool stop_turn: If stop_turn returns ``True`` the motor stops turning. Depending on ``brake`` it stops by holding or not holding the motor. The motor will not stop until it turns the desired distance or stop_turn returns True. Accuracy is much better over a USB connection than with bluetooth... """ tacho_limit = tacho_units if tacho_limit < 0: raise ValueError("tacho_units must be greater than 0!") # TODO Calibrate the new values for ip socket latency. if self.method == "bluetooth": threshold = 70 elif self.method == "usb": threshold = 5 elif self.method == "ipbluetooth": threshold = 80 elif self.method == "ipusb": threshold = 15 else: threshold = 30 # compromise tacho = self.get_tacho() state = self._get_new_state() # Update modifiers even if they aren't used, might have been changed state.power = power if not emulate: state.tacho_limit = tacho_limit logger.debug("updating motor information") self._set_state(state) direction = 1 if power > 0 else -1 logger.debug("tachocount: %s", tacho) current_time = time.time() last_time = time.time() tacho_target = tacho.get_target(tacho_limit, direction) blocked = False sleep_time = self._eta(tacho, tacho_target, power) / 2 try: while not stop_turn(): time.sleep(0.1) if current_time - last_time < sleep_time: current_time = time.time() continue else: if not blocked: # if still blocked, don't reset the counter last_tacho = tacho last_time = current_time current_time = time.time() tacho = self.get_tacho() blocked = self._is_blocked(tacho, last_tacho, direction) if blocked: logger.debug("not advancing: %s %s", last_tacho, tacho) # The motor can be up to 80+ degrees in either direction from target # when using Bluetooth. if current_time - last_time > timeout: if tacho.is_near(tacho_target, threshold): break else: raise BlockedException("Blocked!") else: logger.debug("advancing: %s %s", last_tacho, tacho) if tacho.is_near(tacho_target, threshold) or tacho.is_greater( tacho_target, direction ): break sleep_time = self._eta(tacho, tacho_target, power) / 2 finally: if brake: self.brake() else: self.idle() class Motor(BaseMotor): def __init__(self, brick, port): self.brick = brick self.port = port self._read_state() self.sync = 0 self.turn_ratio = 0 self.method = brick._sock.type def _set_state(self, state): logger.debug("setting brick output state: %s", state) list_state = [self.port] + state.to_list() self.brick.set_output_state(*list_state) self._state = state def _read_state(self): logger.debug("getting brick output state") values = self.brick.get_output_state(self.port) self._state, tacho = get_tacho_and_state(values) return self._state, tacho # def get_tacho_and_state here would allow tacho manipulation def _get_state(self): """Returns a copy of the current motor state for manipulation.""" return OutputState(self._state.to_list()) def _get_new_state(self): state = self._get_state() if self.sync: state.mode = Mode.ON | Mode.REGULATED state.regulation_mode = RegulationMode.SYNC state.turn_ratio = self.turn_ratio else: state.mode = Mode.ON | Mode.REGULATED state.regulation_mode = RegulationMode.SPEED state.run_state = RunState.RUNNING state.tacho_limit = LIMIT_RUN_FOREVER return state def get_tacho(self): return self._read_state()[1] def reset_position(self, relative): """Resets the counters. Relative can be True or False""" self.brick.reset_motor_position(self.port, relative) def run(self, power=100, regulated=False): """Tells the motor to run continuously. :param int power: Motor power or speed if `regulated`. :param bool regulated: If ``True``, use speed regulation. """ state = self._get_new_state() state.power = power if not regulated: state.mode = Mode.ON self._set_state(state) def brake(self): """Holds the motor in place""" state = self._get_new_state() state.power = 0 state.mode = Mode.ON | Mode.BRAKE | Mode.REGULATED self._set_state(state) def idle(self): """Tells the motor to stop whatever it's doing. It also desyncs it.""" state = self._get_new_state() state.power = 0 state.mode = Mode.IDLE state.regulation_mode = RegulationMode.IDLE state.run_state = RunState.IDLE self._set_state(state) def weak_turn(self, power, tacho_units): """Tries to turn a motor for the specified distance. This function returns immediately, and it's not guaranteed that the motor turns that distance. This is an interface to use tacho_limit without RegulationMode.SPEED """ tacho_limit = tacho_units state = self._get_new_state() # Update modifiers even if they aren't used, might have been changed state.mode = Mode.ON state.regulation_mode = RegulationMode.IDLE state.power = power state.tacho_limit = tacho_limit logger.debug("updating motor information") self._set_state(state) def _eta(self, current, target, power): """Returns time in seconds. Do not trust it too much""" tacho = abs(current.tacho_count - target.tacho_count) return (float(tacho) / abs(power)) / 5 def _is_blocked(self, tacho, last_tacho, direction): """Returns if any of the engines is blocked""" return direction * (last_tacho.tacho_count - tacho.tacho_count) >= 0 class SynchronizedMotors(BaseMotor): """The object used to make two motors run in sync. Many objects may be present at the same time but they can't be used at the same time. Warning! Movement methods reset tacho counter. THIS CODE IS EXPERIMENTAL!!! """ def __init__(self, leader, follower, turn_ratio): """Turn ratio can be >= 0 only! If you want to have it reversed, change motor order. """ if follower.brick != leader.brick: raise ValueError("motors belong to different bricks") self.leader = leader self.follower = follower # Being from the same brick, they both have the same com method. self.method = self.leader.method if turn_ratio < 0: raise ValueError("turn ratio < 0, change motor order instead") if self.leader.port == self.follower.port: raise ValueError("The same motor passed twice") elif self.leader.port.value > self.follower.port.value: self.turn_ratio = turn_ratio else: logger.debug("reversed") self.turn_ratio = -turn_ratio def _get_new_state(self): return self.leader._get_new_state() def _set_state(self, state): self.leader._set_state(state) self.follower._set_state(state) def get_tacho(self): leadertacho = self.leader.get_tacho() followertacho = self.follower.get_tacho() return SynchronizedTacho(leadertacho, followertacho) def reset_position(self, relative): """Resets the counters. Relative can be True or False""" self.leader.reset_position(relative) self.follower.reset_position(relative) def _enable(self): # This works as expected. I'm not sure why. # self._disable() self.reset_position(True) self.leader.sync = True self.follower.sync = True self.leader.turn_ratio = self.turn_ratio self.follower.turn_ratio = self.turn_ratio def _disable(self): # This works as expected. (tacho is reset ok) self.leader.sync = False self.follower.sync = False # self.reset_position(True) self.leader.idle() self.follower.idle() # self.reset_position(True) def run(self, power=100): """Warning! After calling this method, make sure to call idle. The motors are reported to behave wildly otherwise. """ self._enable() self.leader.run(power, True) self.follower.run(power, True) def brake(self): self._disable() # reset the counters self._enable() self.leader.brake() # brake both motors at the same time self.follower.brake() self._disable() # now brake as usual self.leader.brake() self.follower.brake() def idle(self): self._disable() def turn(self, power, tacho_units, brake=True, timeout=1): self._enable() # non-emulation is a nightmare, tacho is being counted differently try: if power < 0: self.leader, self.follower = self.follower, self.leader BaseMotor.turn(self, power, tacho_units, brake, timeout, emulate=True) finally: if power < 0: self.leader, self.follower = self.follower, self.leader def _eta(self, tacho, target, power): return self.leader._eta(tacho.leader_tacho, target.leader_tacho, power) def _is_blocked(self, tacho, last_tacho, direction): # no need to check both, they're synced return self.leader._is_blocked( tacho.leader_tacho, last_tacho.leader_tacho, direction ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/nxt/sensor/__init__.py0000644000000000000000000000626514155725071015133 0ustar00# nxt.sensor module -- Classes to read LEGO Mindstorms NXT sensors # Copyright (C) 2006,2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner, Paulo Vieira, rhn # Copyright (C) 2010 Marcus Wanner # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import enum __all__ = ["Port", "Type", "Mode", "Sensor"] class Port(enum.Enum): """Input port identifier. The prefix is needed because a Python identifier can not start with a digit. """ S1 = 0 """Sensor port 1.""" S2 = 1 """Sensor port 2.""" S3 = 2 """Sensor port 3.""" S4 = 3 """Sensor port 4.""" class Type(enum.Enum): """Sensor type.""" NO_SENSOR = 0 """No sensor is connected.""" SWITCH = 1 """Touch sensor.""" TEMPERATURE = 2 """RCX temperature sensor.""" REFLECTION = 3 """RCX light sensor.""" ANGLE = 4 """RCX rotation sensor.""" LIGHT_ACTIVE = 5 """Light sensor with light active.""" LIGHT_INACTIVE = 6 """Light sensor with light off.""" SOUND_DB = 7 """Sound sensor (unadjusted).""" SOUND_DBA = 8 """Sound sensor (adjusted).""" CUSTOM = 9 """Custom sensor (unused).""" LOW_SPEED = 10 """Low speed digital sensor.""" LOW_SPEED_9V = 11 """Low speed digital sensor with 9V supply voltage.""" HIGH_SPEED = 12 """High speed sensor.""" COLOR_FULL = 13 """NXT color sensor in full color mode (color sensor mode).""" COLOR_RED = 14 """NXT color sensor with red light on (light sensor mode).""" COLOR_GREEN = 15 """NXT color sensor with green light on (light sensor mode).""" COLOR_BLUE = 16 """NXT color sensor in with blue light on (light sensor mode).""" COLOR_NONE = 17 """NXT color sensor in with light off (light sensor mode).""" COLOR_EXIT = 18 """NXT color sensor internal state.""" class Mode(enum.Enum): """Sensor mode.""" RAW = 0x00 """Raw value, from 0 to 1023.""" BOOL = 0x20 """Boolean value, 0 or 1.""" EDGE = 0x40 """Count number of transitions.""" PULSE = 0x60 """Count number of pulse.""" PERCENT = 0x80 """Value from 0 to 100.""" CELSIUS = 0xA0 """Temperature in degree Celsius.""" FAHRENHEIT = 0xC0 """Temperature in degree Fahrenheit.""" ROTATION = 0xE0 """RCX rotation sensor mode.""" class Sensor: """Sensor base class.""" def __init__(self, brick, port): self._brick = brick self._port = port def set_input_mode(self, sensor_type, sensor_mode): """Set sensor input mode. :param Type sensor_type: Sensor type. :param Mode sensor_mode: Sensor mode. """ self._brick.set_input_mode(self._port, sensor_type, sensor_mode) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735691247.2309268 nxt_python-3.5.1/nxt/sensor/analog.py0000644000000000000000000000516014735105757014635 0ustar00# nxt.sensor.analog module -- submodule for use with analog sensors # Copyright (C) 2006,2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner, Paulo Vieira, rhn # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import time from dataclasses import dataclass import nxt.sensor class InvalidReading(Exception): """Exception raised on timeout trying to get valid readings.""" pass @dataclass class RawReading: """A object holding the raw sensor values for a sensor.""" #: Input port identifier. port: nxt.sensor.Port #: ``True`` if the value is valid, else ``False``. valid: bool #: Always ``False``, there is no calibration in NXT firmware. calibrated: bool #: Sensor type. sensor_type: nxt.sensor.Type #: Sensor mode. sensor_mode: nxt.sensor.Mode #: Raw analog to digital converter value. raw_value: int #: Normalized value. normalized_value: int #: Scaled value. scaled_value: int #: Always normalized value, there is no calibration in NXT firmware. calibrated_value: int class BaseAnalogSensor(nxt.sensor.Sensor): """Object for analog sensors.""" def get_input_values(self): """Get raw sensor readings. :return: An object with the read values. :rtype: RawReading """ return RawReading(*self._brick.get_input_values(self._port)) def get_valid_input_values(self): """Wait until input is valid, then get raw sensor readings. :return: An object with the read values. :rtype: RawReading :raises InvalidReading: On timeout trying to get valid readings. """ tries = 10 tries_delay_s = 0.1 readings = self.get_input_values() while not readings.valid: tries -= 1 if tries == 0: raise InvalidReading() time.sleep(tries_delay_s) readings = self.get_input_values() return readings def reset_input_scaled_value(self): """Reset sensor scaled value. This can be used to reset accumulated value for some sensor modes. """ self._brick.reset_input_scaled_value(self._port) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704409096.3199499 nxt_python-3.5.1/nxt/sensor/digital.py0000644000000000000000000002333014545634010014774 0ustar00# nxt.sensor module -- Classes to read LEGO Mindstorms NXT sensors # Copyright (C) 2006,2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner, Paulo Vieira, rhn # Copyright (C) 2010,2011,2012 Marcus Wanner # Copyright (C) 2023 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import logging import struct import time import nxt.sensor from nxt.error import DirectProtocolError, I2CError, I2CPendingError logger = logging.getLogger(__name__) class SensorInfo: def __init__(self, version, product_id, sensor_type): self.version = version self.product_id = product_id self.sensor_type = sensor_type def clarifybinary(self, instr, label): outstr = "" outstr += label + ": `" + instr + "`\n" for char in instr: outstr += hex(ord(char)) + ", " outstr += "\n" return outstr def __str__(self): outstr = "" outstr += self.clarifybinary(str(self.version), "Version") outstr += self.clarifybinary(str(self.product_id), "Product ID") outstr += self.clarifybinary(str(self.sensor_type), "Type") return outstr class BaseDigitalSensor(nxt.sensor.Sensor): """Object for digital sensors. :param bool check_compatible: Check sensor class match the connected sensor. If `check_compatible` is ``True``, queries the sensor for its name and print a warning if the wrong sensor class is used. `I2C_ADDRESS` is the dictionary storing name to I2C address mappings. It should be updated in every subclass. When subclassing this class, make sure to call :func:`add_compatible_sensor` to add compatible sensor data. """ I2C_DEV = 0x02 I2C_ADDRESS = { "version": (0x00, "8s"), "product_id": (0x08, "8s"), "sensor_type": (0x10, "8s"), # "factory_zero": (0x11, 1), # is this really correct? "factory_scale_factor": (0x12, "B"), "factory_scale_divisor": (0x13, "B"), } def __init__(self, brick, port, check_compatible=True): super().__init__(brick, port) self.set_input_mode(nxt.sensor.Type.LOW_SPEED_9V, nxt.sensor.Mode.RAW) self.last_poll = time.time() self.poll_delay = 0.01 time.sleep(0.1) # Give I2C time to initialize # Don't do type checking if this class has no compatible sensors listed. try: self.compatible_sensors except AttributeError: check_compatible = False if check_compatible: sensor = self.get_sensor_info() if sensor not in self.compatible_sensors: logger.warning( "wrong sensor class chosen for sensor %s on port %s", sensor.product_id, port, ) logger.warning( " You may be using the wrong type of sensor or may have " "connected the cable incorrectly. If you are sure you're using the " "correct sensor class for the sensor, this message is likely in " "error and you should disregard it and file a bug report, " "including the output of get_sensor_info(). This message can be " "suppressed by passing check_compatible=False when creating the " "sensor object." ) def _ls_get_status(self, size): for n in range(30): # https://code.google.com/p/nxt-python/issues/detail?id=35 try: b = self._brick.ls_get_status(self._port) if b >= size: return b except I2CPendingError: pass raise I2CError("ls_get_status timeout") def _i2c_command(self, address, value, format): """Write one or several values to an I2C register. :param int address: I2C register address. :param tuple value: Tuple of values to write. :param str format: Format string using :mod:`struct` syntax. """ value = struct.pack(format, *value) msg = bytes((self.I2C_DEV, address)) + value now = time.time() if self.last_poll + self.poll_delay > now: diff = now - self.last_poll time.sleep(self.poll_delay - diff) self.last_poll = time.time() self._brick.ls_write(self._port, msg, 0) def _i2c_query(self, address, format): """Read one or several values from an I2C register. :param int address: I2C register address. :param str format: Format string using :mod:`struct` syntax. :return: Read values in a tuple. :rtype: tuple """ size = struct.calcsize(format) msg = bytes((self.I2C_DEV, address)) now = time.time() if self.last_poll + self.poll_delay > now: diff = now - self.last_poll time.sleep(self.poll_delay - diff) self.last_poll = time.time() self._brick.ls_write(self._port, msg, size) try: self._ls_get_status(size) finally: # we should clear the buffer no matter what happens data = self._brick.ls_read(self._port) if len(data) < size: raise I2CError("Read failure: Not enough bytes") data = struct.unpack(format, data[-size:]) return data def read_value(self, name): """Read one or several values from the sensor. :param str name: Name of the values to read. :return: Read values in a tuple. :rtype: tuple The `name` parameter is an index inside `I2C_ADDRESS` dictionary, which gives the corresponding I2C register address and format string. """ address, fmt = self.I2C_ADDRESS[name] for n in range(3): try: return self._i2c_query(address, fmt) except DirectProtocolError: pass raise I2CError("read_value timeout") def write_value(self, name, value): """Write one or several values to the sensor. :param str name: Name of the values to write. :param tuple value: Tuple of values to write. The `name` parameter is an index inside `I2C_ADDRESS` dictionary, which gives the corresponding I2C register address and format string. """ address, fmt = self.I2C_ADDRESS[name] self._i2c_command(address, value, fmt) def get_sensor_info(self): version = self.read_value("version")[0].decode("windows-1252").split("\0")[0] product_id = ( self.read_value("product_id")[0].decode("windows-1252").split("\0")[0] ) sensor_type = ( self.read_value("sensor_type")[0].decode("windows-1252").split("\0")[0] ) return SensorInfo(version, product_id, sensor_type) @classmethod def add_compatible_sensor(cls, version, product_id, sensor_type): """Adds an entry in the compatibility table for the sensor. :param version: Sensor version, or ``None`` for default class. :type version: str or None :param product_id: Product identifier, or ``None`` for default class. :type product_id: str or None :param str sensor_type: Sensor type """ try: cls.compatible_sensors except AttributeError: cls.compatible_sensors = [] finally: cls.compatible_sensors.append( _SCompatibility(version, product_id, sensor_type) ) _add_mapping(cls, version, product_id, sensor_type) class _SCompatibility(SensorInfo): """An object that helps manage the sensor mappings.""" def __eq__(self, other): if self.product_id is None: return self.product_id == other.product_id elif self.version is None: return ( self.product_id == other.product_id and self.sensor_type == other.sensor_type ) else: return ( self.version == other.version and self.product_id == other.product_id and self.sensor_type == other.sensor_type ) sensor_mappings = {} def _add_mapping(cls, version, product_id, sensor_type): if product_id not in sensor_mappings: sensor_mappings[product_id] = {} models = sensor_mappings[product_id] if sensor_type is None: if sensor_type in models: raise ValueError("Already registered!") models[sensor_type] = cls return if sensor_type not in models: models[sensor_type] = {} versions = models[sensor_type] if version in versions: raise ValueError("Already registered!") else: versions[version] = cls class SearchError(Exception): pass def find_class(info): """Returns an appropriate class. :param SensorInfo info: Information read from the sensor. :return: Class corresponding to sensor. :rtype: BaseDigitalSensor :raises SearchError: When no class found. """ dic = sensor_mappings for val, msg in zip( (info.product_id, info.sensor_type, info.version), ("Vendor", "Model", "Version"), ): if val in dic: dic = dic[val] elif None in dic: dic = dic[None] else: raise SearchError(msg + " not found") return dic[info.sensor_type][None] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736270404.884913 nxt_python-3.5.1/nxt/sensor/generic.py0000644000000000000000000002325214737261105015002 0ustar00# nxt.sensor.generic module -- Support for LEGO Mindstorms NXT sensors # Copyright (C) 2006,2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner, Paulo Vieira, rhn # Copyright (C) 2010 melducky, Marcus Wanner # Copyright (C) 2022 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. """The :mod:`nxt.sensor.generic` module supports the sensors manufactured by LEGO.""" import enum from nxt.sensor import Mode, Type from nxt.sensor.analog import BaseAnalogSensor from nxt.sensor.digital import BaseDigitalSensor class Touch(BaseAnalogSensor): """LEGO Mindstorms NXT touch sensor, part number 53793.""" def __init__(self, brick, port): super().__init__(brick, port) self.set_input_mode(Type.SWITCH, Mode.BOOL) def is_pressed(self): """Get sensor pressed state. :return: ``True`` if the sensor is pressed, else ``False``. :rtype: bool """ return bool(self.get_valid_input_values().scaled_value) get_sample = is_pressed class Light(BaseAnalogSensor): """LEGO Mindstorms NXT light sensor, part number 55969. This sensor is included in the first version of the Mindstorms NXT set and in the Education NXT set. Not to be confused with the color sensor included in the NXT 2.0 set. :param bool illuminated: Initial illuminated state. """ def __init__(self, brick, port, illuminated=True): super().__init__(brick, port) self.set_illuminated(illuminated) def set_illuminated(self, active): """Set illuminated state. :param bool active: ``True`` to activate light. """ if active: type_ = Type.LIGHT_ACTIVE else: type_ = Type.LIGHT_INACTIVE self.set_input_mode(type_, Mode.RAW) def get_lightness(self): """Get detected light level. :return: Detected light level between 0 and 1023. :rtype: int """ return self.get_valid_input_values().scaled_value get_sample = get_lightness class Sound(BaseAnalogSensor): """LEGO Mindstorms NXT sound sensor, part number 55963. :param bool adjusted: Initial adjusted state. """ def __init__(self, brick, port, adjusted=True): super().__init__(brick, port) self.set_adjusted(adjusted) def set_adjusted(self, active): """Set adjusted state. When activated, the sensitivity of the sensor is adjusted to human perception. :param bool active: ``True`` to activate adjustment. """ if active: type_ = Type.SOUND_DBA else: type_ = Type.SOUND_DB self.set_input_mode(type_, Mode.RAW) def get_loudness(self): """Get detected sound level. :return: Detected sound level between 0 and 1023. :rtype: int """ return self.get_valid_input_values().scaled_value get_sample = get_loudness class Ultrasonic(BaseDigitalSensor): """LEGO Mindstorms NXT ultrasonic distance sensor, part number 53792.""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "measurement_units": (0x14, "7s"), "continuous_measurement_interval": (0x40, "B"), "command": (0x41, "B"), "measurement_byte_0": (0x42, "B"), "measurement_byte_1": (0x43, "B"), "measurement_byte_2": (0x44, "B"), "measurement_byte_3": (0x45, "B"), "measurement_byte_4": (0x46, "B"), "measurement_byte_5": (0x47, "B"), "measurement_byte_6": (0x48, "B"), "measurement_byte_7": (0x49, "B"), "measurements": (0x42, "8B"), "actual_scale_factor": (0x51, "B"), "actual_scale_divisor": (0x52, "B"), } ) class Command(enum.Enum): """Sensor command.""" OFF = 0x00 """Turn sensor OFF.""" SINGLE_SHOT = 0x01 """Enable single shot mode.""" CONTINUOUS_MEASUREMENT = 0x02 """Enable continuous measurement mode (default).""" EVENT_CAPTURE = 0x03 """Enable event capture mode, to measure whether any other sensor is in the neighbourhood.""" REQUEST_WARM_RESET = 0x04 """Reset sensor.""" def __init__(self, brick, port, check_compatible=True): super().__init__(brick, port, check_compatible) def get_distance(self): """Get distance to detected object. :return: Distance in cm. :rtype: int """ return self.read_value("measurement_byte_0")[0] get_sample = get_distance def get_measurement_units(self): """Get measurement units. :return: String representing the measurement units, should be "10E-2m". :rtype: str """ return ( self.read_value("measurement_units")[0] .decode("windows-1252") .split("\0")[0] ) def get_all_measurements(self): """Get all the measurements. :return: Eight distances in cm. :rtype: (int, int, int, int, int, int, int, int) """ return self.read_value("measurements") def get_measurement_no(self, number): """Get one of the measurements. :param int number: Measurement number. :return: Distance in cm. :rtype: int """ return self.read_value(f"measurement_byte_{number}")[0] def command(self, command): """Send a command to the sensor. :param Command command: Command to send. """ self.write_value("command", (command.value,)) def get_interval(self): """Get measurement interval, unknown unit. :return: Content of interval measurement register. :rtype: int """ return self.read_value("continuous_measurement_interval")[0] def set_interval(self, interval): """Set measurement interval, unknown unit. :param int interval: New content of interval measurement register. """ self.write_value("continuous_measurement_interval", (interval,)) Ultrasonic.add_compatible_sensor(None, "LEGO", "Sonar") # Tested with version 'V1.0' class Color(BaseAnalogSensor): """LEGO Mindstorms NXT color sensor, part number 64892. This sensor is included in the Mindstorms NXT 2.0 set. Not to be confused with the light sensor included in the first version or in the Education set. """ class DetectedColor(enum.IntEnum): """Color detected by the sensor. This is an :class:`enum.IntEnum` for backward compatibility. """ BLACK = 1 """Black or low intensity.""" BLUE = 2 """Blue.""" GREEN = 3 """Green.""" YELLOW = 4 """Yellow.""" RED = 5 """Red.""" WHITE = 6 """White.""" def __init__(self, brick, port): super().__init__(brick, port) self.set_light_color(Type.COLOR_FULL) def set_light_color(self, color): """Set the light color. :param Type color: Light color, or off, must be one of the Type.COLOR_* values. """ self.set_input_mode(color, Mode.RAW) def get_light_color(self): """Get the light color previously set. :return: Light color, one of the Type.COLOR_* values. :rtype: Type """ return self.get_valid_input_values().sensor_type def get_reflected_light(self, color): """Get detected light level. :param Type color: Light color, or off, must be one of the Type.COLOR_* values. :return: Detected light level between 0 and 1023. :rtype: int """ self.set_light_color(color) return self.get_valid_input_values().scaled_value def get_color(self): """Get detected color. :return: Detected color. :rtype: DetectedColor This also sets the sensor mode to color detection (light is cycling quickly to determine color). """ self.get_reflected_light(Type.COLOR_FULL) return self.DetectedColor(self.get_valid_input_values().scaled_value) get_sample = get_color class Temperature(BaseDigitalSensor): """LEGO Mindstorms NXT temperature sensor, part number 62840.""" # This is actually a TI TMP275 chip: https://www.ti.com/product/tmp275 I2C_DEV = 0x98 I2C_ADDRESS = {"raw_value": (0x00, ">h")} def __init__(self, brick, port): # This sensor does not follow the convention of having version/vendor/product at # I2C registers 0x00/0x08/0x10, so check_compatible is always False. super().__init__(brick, port, False) def _get_raw_value(self): """Get raw unscaled value. :return: Unscaled value. :rtype: int """ # This is a 12-bit value. return self.read_value("raw_value")[0] >> 4 def get_deg_c(self): """Get the temperature in degrees Celsius. :return: Temperature in degrees Celsius. :rtype: float """ v = self._get_raw_value() return round(v / 16, 1) def get_deg_f(self): """Get the temperature in degrees Fahrenheit. :return: Temperature in degrees Fahrenheit. :rtype: float """ v = self._get_raw_value() return round(9 / 5 * v / 16 + 32, 1) get_sample = get_deg_c ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735691247.2269268 nxt_python-3.5.1/nxt/sensor/hitechnic.py0000644000000000000000000010046514735105757015336 0ustar00# nxt.sensor.hitechnic module -- Classes to read HiTechnic sensors # Copyright (C) 2006,2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner, Paulo Vieira, rhn # Copyright (C) 2010 rhn, Marcus Wanner, melducky, Samuel Leeman-Munk # Copyright (C) 2011 jerradgenson, Marcus Wanner # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from enum import Enum from nxt.sensor import Mode, Type from nxt.sensor.analog import BaseAnalogSensor from nxt.sensor.digital import BaseDigitalSensor class Compass(BaseDigitalSensor): """Hitechnic compass sensor.""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "mode": (0x41, "B"), "heading": (0x42, "B"), "adder": (0x43, "B"), } ) class Modes: MEASUREMENT = 0x00 CALIBRATION = 0x43 CALIBRATION_FAILED = 0x02 def get_heading(self): """Returns heading from North in degrees.""" two_degree_heading = self.read_value("heading")[0] adder = self.read_value("adder")[0] heading = two_degree_heading * 2 + adder return heading get_sample = get_heading def get_relative_heading(self, target=0): rheading = self.get_sample() - target if rheading > 180: rheading -= 360 elif rheading < -180: rheading += 360 return rheading def is_in_range(self, minval, maxval): """ This deserves a little explanation: if max > min, it's straightforward, but if min > max, it switches the values of max and min and returns true if heading is NOT between the new max and min """ if minval > maxval: (maxval, minval) = (minval, maxval) inverted = True else: inverted = False heading = self.get_sample() in_range = (heading > minval) and (heading < maxval) return bool(inverted) ^ bool(in_range) def get_mode(self): return self.read_value("mode")[0] def set_mode(self, mode): if mode != self.Modes.MEASUREMENT and mode != self.Modes.CALIBRATION: raise ValueError("Invalid mode specified: " + str(mode)) self.write_value("mode", (mode,)) Compass.add_compatible_sensor( None, "HiTechnc", "Compass " ) # Tested with version '\xfdV1.23 ' Compass.add_compatible_sensor( None, "HITECHNC", "Compass " ) # Tested with version '\xfdV2.1 ' class Accelerometer(BaseDigitalSensor): """Object for Accelerometer sensors. Thanks to Paulo Vieira.""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "x_axis_high": (0x42, "b"), "y_axis_high": (0x43, "b"), "z_axis_high": (0x44, "b"), "xyz_short": (0x42, "3b"), "all_data": (0x42, "3b3B"), } ) class Acceleration: def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z def __init__(self, brick, port, check_compatible=True): super().__init__(brick, port, check_compatible) def get_acceleration(self): """Returns the acceleration along x, y, z axes. 200 => 1g.""" xh, yh, zh, xl, yl, zl = self.read_value("all_data") x = xh << 2 | xl y = yh << 2 | yl z = zh << 2 | zl return self.Acceleration(x, y, z) get_sample = get_acceleration Accelerometer.add_compatible_sensor(None, "HiTechnc", "Accel. ") Accelerometer.add_compatible_sensor( None, "HITECHNC", "Accel. " ) # Tested with version '\xfdV1.1 ' class IRReceiver(BaseDigitalSensor): """ Object for HiTechnic IRReceiver sensors for use with LEGO Power Functions IR Remotes. Coded to HiTechnic's specs for the sensor but not tested. Please report whether this worked for you or not! """ I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "m1A": (0x42, "b"), "m1B": (0x43, "b"), "m2A": (0x44, "b"), "m2B": (0x45, "b"), "m3A": (0x46, "b"), "m3B": (0x47, "b"), "m4A": (0x48, "b"), "m4B": (0x49, "b"), "all_data": (0x42, "8b"), } ) class SpeedReading: def __init__(self, m1A, m1B, m2A, m2B, m3A, m3B, m4A, m4B): ( self.m1A, self.m1B, self.m2A, self.m2B, self.m3A, self.m3B, self.m4A, self.m4B, ) = (m1A, m1B, m2A, m2B, m3A, m3B, m4A, m4B) self.channel_1 = (m1A, m1B) self.channel_2 = (m2A, m2B) self.channel_3 = (m3A, m3B) self.channel_4 = (m4A, m4B) def __init__(self, brick, port, check_compatible=True): super().__init__(brick, port, check_compatible) def get_speeds(self): """ Returns the motor speeds for motors A and B on channels 1-4. Values are -128, -100, -86, -72, -58, -44, -30, -16, 0, 16, 30, 44, 58, 72, 86 and 100. -128 specifies motor brake mode. Note that no motors are actually being controlled here! """ m1A, m1B, m2A, m2B, m3A, m3B, m4A, m4B = self.read_value("all_data") return self.SpeedReading(m1A, m1B, m2A, m2B, m3A, m3B, m4A, m4B) get_sample = get_speeds IRReceiver.add_compatible_sensor(None, "HiTechnc", "IRRecv ") IRReceiver.add_compatible_sensor(None, "HITECHNC", "IRRecv ") class IRSeekerv2(BaseDigitalSensor): """ Object for HiTechnic IRSeeker sensors. Coded to HiTechnic's specs for the sensor but not tested. Please report whether this worked for you or not! """ I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "dspmode": (0x41, "B"), "DC_direction": (0x42, "B"), "DC_sensor_1": (0x43, "B"), "DC_sensor_2": (0x44, "B"), "DC_sensor_3": (0x45, "B"), "DC_sensor_4": (0x46, "B"), "DC_sensor_5": (0x47, "B"), "DC_sensor_mean": (0x48, "B"), "all_DC": (0x42, "7B"), "AC_direction": (0x49, "B"), "AC_sensor_1": (0x4A, "B"), "AC_sensor_2": (0x4B, "B"), "AC_sensor_3": (0x4C, "B"), "AC_sensor_4": (0x4D, "B"), "AC_sensor_5": (0x4E, "B"), "all_AC": (0x49, "6B"), } ) I2C_DEV = 0x10 # different from standard 0x02 class DSPModes: # Modes for modulated (AC) data. AC_DSP_1200Hz = 0x00 AC_DSP_600Hz = 0x01 class _data: def get_dir_brightness(self, direction): """Gets the brightness of a given direction (1-9).""" if direction % 2 == 1: # if it's an odd number val = getattr(self, "sensor_%d" % ((direction - 1) // 2 + 1)) else: val = ( getattr(self, f"sensor_{direction // 2}") + getattr(self, f"sensor_{direction // 2 + 1}") ) / 2 return val class DCData(_data): def __init__( self, direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5, sensor_mean, ): ( self.direction, self.sensor_1, self.sensor_2, self.sensor_3, self.sensor_4, self.sensor_5, self.sensor_mean, ) = ( direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5, sensor_mean, ) class ACData(_data): def __init__(self, direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5): ( self.direction, self.sensor_1, self.sensor_2, self.sensor_3, self.sensor_4, self.sensor_5, ) = (direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5) def __init__(self, brick, port, check_compatible=True): super().__init__(brick, port, check_compatible) def get_dc_values(self): """ Returns the unmodulated (DC) values. """ ( direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5, sensor_mean, ) = self.read_value("all_DC") return self.DCData( direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5, sensor_mean ) def get_ac_values(self): """ Returns the modulated (AC) values. 600Hz and 1200Hz modes can be selected between by using the set_dsp_mode() function. """ direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5 = self.read_value( "all_AC" ) return self.ACData(direction, sensor_1, sensor_2, sensor_3, sensor_4, sensor_5) def get_dsp_mode(self): return self.read_value("dspmode")[0] def set_dsp_mode(self, mode): self.write_value("dspmode", (mode,)) get_sample = get_ac_values IRSeekerv2.add_compatible_sensor(None, "HiTechnc", "NewIRDir") IRSeekerv2.add_compatible_sensor(None, "HITECHNC", "NewIRDir") class EOPD(BaseAnalogSensor): """ Object for HiTechnic Electro-Optical Proximity Detection sensors. """ # To be divided by processed value. _SCALE_CONSTANT = 250 # Maximum distance the sensor can detect. _MAX_DISTANCE = 1023 def __init__(self, brick, port): super().__init__(brick, port) from math import sqrt self.sqrt = sqrt def set_range_long(self): """ Choose this mode to increase the sensitivity of the EOPD sensor by approximately 4x. May cause sensor overload. """ self.set_input_mode(Type.LIGHT_ACTIVE, Mode.RAW) def set_range_short(self): """ Choose this mode to prevent the EOPD sensor from being overloaded by white objects. """ self.set_input_mode(Type.LIGHT_INACTIVE, Mode.RAW) def get_raw_value(self): """Unscaled value read from sensor.""" return self._MAX_DISTANCE - self.get_valid_input_values().raw_value def get_processed_value(self): """Derived from the square root of the raw value.""" return self.sqrt(self.get_raw_value()) def get_scaled_value(self): """ Returns a value that will scale linearly as distance from target changes. This is the method that should generally be called to get EOPD sensor data. """ try: result = self._SCALE_CONSTANT / self.get_processed_value() return result except ZeroDivisionError: return self._SCALE_CONSTANT get_sample = get_scaled_value class Colorv2(BaseDigitalSensor): """ Object for HiTechnic Color v2 Sensors. Coded to HiTechnic's specs for the sensor but not tested. Please report whether this worked for you or not! """ I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "mode": (0x41, "B"), "number": (0x42, "B"), "red": (0x43, "B"), "green": (0x44, "B"), "blue": (0x45, "B"), "white": (0x46, "B"), "index": (0x47, "B"), "normred": (0x48, "B"), "normgreen": (0x49, "B"), "normblue": (0x4A, "B"), "all_data": (0x42, "9B"), "rawred": (0x42, " dict[str, int]: """ Get analog input pins (A0-A3) result in raw bits :return: Map of pin name, pin value (bits) """ analog_in_pin_name_map = [ ("analog_a0", "a0"), ("analog_a1", "a1"), ("analog_a2", "a2"), ("analog_a3", "a3"), ] analog_raw_map = {} for pin in analog_in_pin_name_map: raw = self.read_value(pin[0])[0] low = (raw & 0xFF00) >> 8 high = raw & 0x00FF analog_input = low + high * 4 analog_raw_map[pin[1]] = analog_input return analog_raw_map def get_analog_volts(self, voltage_reference: float = 3.3) -> dict[str, float]: """ Get analog input pins (A0-A3) results in volts. Resolution to ~3mV (voltage reference * (1/1023) volts) :param voltage_reference: optionally provide measured voltage from 3.3V regulator for more accurate calculations :return: Map of pin name, pin voltage (in volts) """ analog_raw_map = self.get_analog() analog_voltage_map = {} for item in analog_raw_map: analog_voltage_map[item] = ( analog_raw_map[item] * self.ANALOG_LSB * voltage_reference ) return analog_voltage_map @staticmethod def _byte_to_boolean_list(integer: int, reverse=False) -> list[bool]: # Converts byte to boolean string, inverts string to correct bit order if # needed, converts chars to boolean list if reverse: return [bit == "1" for bit in f"{integer:08b}"[::-1]] return [bit == "1" for bit in f"{integer:08b}"] @staticmethod def _boolean_list_to_byte(boolean_list: list[bool], reverse=False) -> int: if len(boolean_list) != 8: raise RuntimeError("List must be 8 booleans in length") if reverse: boolean_list.reverse() output_byte = 0 for bit in range(0, 8): output = (2**bit) * boolean_list[bit] output_byte += output return output_byte def get_digital(self) -> dict[str, bool]: """ Get digital input pins (D0-D7) :return: Boolean list, LSB first. """ digital_in = self.read_value("digital_in")[0] boolean_list = self._byte_to_boolean_list(digital_in, reverse=True) digital_in_map = {} for x in range(0, 8): digital_in_map[f"b{x}"] = boolean_list[x] return digital_in_map def set_digital(self, pins: list[bool]): """ Set digital output pins (D0-D7) :param pins: boolean list (LSB first) """ if len(pins) != 8: raise RuntimeError("Need to specify list of 8 boolean values") self.write_value("digital_out", [self._boolean_list_to_byte(pins)]) def set_digital_byte(self, integer: int, lsb=True): """ Set digital output pins (D0-D7) from byte :param integer: Byte (0-255 inclusive) :param lsb: Whether to output in LSB order (0x01 = pin B0) """ if not (0 <= integer <= 255): raise RuntimeError("Integer must be in range of 0 to 255 inclusive") self.set_digital(self._byte_to_boolean_list(integer, reverse=lsb)) def set_digital_modes(self, modes: list[bool]): """ Set digital pin mode (D0-D7) :param modes: boolean list (LSB first, True = Output, False = Input) """ if len(modes) != 8: raise RuntimeError("Need to specify list of 8 boolean values") self.write_value("digital_dir", [self._boolean_list_to_byte(modes)]) def set_digital_modes_byte(self, mode_int: int, lsb=True): """ Set digital output pins mode (D0-D7) from byte :param mode_int: Byte (0-255 inclusive) :param lsb: Whether to output in LSB order (0x01 = pin B0) """ if not (0 <= mode_int <= 255): raise RuntimeError("Integer must be in range of 0 to 255 inclusive") self.set_digital_modes(self._byte_to_boolean_list(mode_int, reverse=lsb)) def set_strobe_output(self, mode_int: int): """ Strobe output - behaves like any other output, but has strobe signal sent whenever a digital read/write sent. When a digital read is done, it will send a spike on RD (inverse of the current RD pin state), when a digital write is done, it will send a spike on WR (inverse of the current WR pin state) This 'digital read' and 'digital write' actions causing a spike applies to the B0-7 pins Bits to write: S0 = 1, S1 = 2 S2 = 4 S3 = 8 RD = 16 WR = 32 :param mode_int: mode_int: Byte (0-63 inclusive) """ if not (0 <= mode_int <= 255): raise RuntimeError("Integer must be in range of 0 to 255 inclusive") self.write_value("strobe_out", [mode_int]) def set_led_output(self, red=False, blue=False): """ Set LED output. True = On, False = Off :param red: Boolean for Red LED :param blue: Boolean for Blue LED """ output_byte = (red * 0x01) + (blue * 0x02) self.write_value("led_out", [output_byte]) def analog_out( self, pin: int, mode: AnalogOutputMode, freq: int, voltage_bits: int ): """ Analog Output Pins :param pin: 0 for O0, 1 for O1 :param mode: 0-5 for various modes, see AnalogOutputMode class :param freq: 0 to 2^13Hz (~8kHz) Note: if 0 provided for wave, will get 1Hz. :param voltage_bits: 0-1023 for 0V to 3.3V (ish) """ if not 0 <= pin <= 1: raise RuntimeError("Pin must be 0 or 1") if not 0 <= voltage_bits <= ((2**10) - 1): raise RuntimeError("Integer must be in range of 0 and (2^10)-1 inclusive") low = voltage_bits & 0b0000000000000011 low_shifted = low << 8 high = voltage_bits & 0b0000001111111100 high_shifted = high >> 2 actual_voltage_bits = low_shifted | high_shifted freq_swapped = (0xFF00 & freq) >> 8 freq_swapped += (0x00FF & freq) << 8 if pin != 0: self.write_value("analog_out1_mode", [mode.value]) self.write_value("analog_out1_freq", [freq_swapped]) self.write_value("analog_out1_volts", [actual_voltage_bits]) else: self.write_value("analog_out0_mode", [mode.value]) self.write_value("analog_out0_freq", [freq_swapped]) self.write_value("analog_out0_volts", [actual_voltage_bits]) def analog_out_voltage( self, pin: int, mode: AnalogOutputMode, freq: int, voltage: float, voltage_reference=3.3, ): """ Analog Output Pins :param pin: 0 for O0, 1 for O1 :param mode: 0-5 for various modes, see AnalogOutputMode class :param freq: 0 to 2^13Hz (~8kHz) Note: if 0 provided for wave, will get 1Hz :param voltage: The desired voltage (between 0 and the voltage reference) :param voltage_reference: Output 1023 in the analog_out mode to find the maximum voltage, enter it here. """ if not 0 <= pin <= 1: raise RuntimeError("Pin must be 0 or 1") voltage_percentile = voltage / voltage_reference voltage_bits = int(voltage_percentile * 1023) self.analog_out(pin, mode, freq, voltage_bits) SuperPro.add_compatible_sensor(None, "HiTechnc", "SuperPro") class ServoCon(BaseDigitalSensor): """ Object for HiTechnic FIRST Servo Controllers. Coded to HiTechnic's specs for the sensor but not tested. Please report whether this worked for you or not! """ I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "status": (0x40, "B"), "steptime": (0x41, "B"), "s1pos": (0x42, "B"), "s2pos": (0x43, "B"), "s3pos": (0x44, "B"), "s4pos": (0x45, "B"), "s5pos": (0x46, "B"), "s6pos": (0x47, "B"), "pwm": (0x48, "B"), } ) class Status: STOPPED = 0x00 # all motors stopped RUNNING = 0x01 # motor(s) moving def __init__(self, brick, port, check_compatible=True): super().__init__(brick, port, check_compatible) def get_status(self): """ Returns the status of the motors. 0 for all stopped, 1 for some running. """ return self.read_value("status")[0] def set_step_time(self, time): """Sets the step time (0-15).""" self.write_value("steptime", (time,)) def set_pos(self, num, pos): """ Sets the position of a server. num is the servo number (1-6), pos is the position (0-255). """ self.write_value("s%dpos" % num, (pos,)) def get_pwm(self): """ Gets the "PWM enable" value. The function of this value is nontrivial and can be found in the documentation for the sensor. """ return self.read_value("pwm")[0] def set_pwm(self, pwm): """ Sets the "PWM enable" value. The function of this value is nontrivial and can be found in the documentation for the sensor. """ self.write_value("pwm", (pwm,)) ServoCon.add_compatible_sensor(None, "HiTechnc", "ServoCon") class MotorCon(BaseDigitalSensor): """Object for HiTechnic FIRST Motor Controllers.""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "m1enctarget": (0x40, ">l"), "m1mode": (0x44, "B"), "m1power": (0x45, "b"), "m2power": (0x46, "b"), "m2mode": (0x47, "B"), "m2enctarget": (0x48, ">l"), "m1enccurrent": (0x4C, ">l"), "m2enccurrent": (0x50, ">l"), "batteryvoltage": (0x54, "2B"), "m1gearratio": (0x56, "b"), "m1pid": (0x57, "3B"), "m2gearratio": (0x5A, "b"), "m2pid": (0x5B, "3B"), } ) class PID_Data: def __init__(self, p, i, d): self.p, self.i, self.d = p, i, d def __init__(self, brick, port, check_compatible=True): super().__init__(brick, port, check_compatible) def set_enc_target(self, mot, val): """Set the encoder target (-2147483648-2147483647) for a motor""" self.write_value("m%denctarget" % mot, (val,)) def get_enc_target(self, mot): """Get the encoder target for a motor""" return self.read_value("m%denctarget" % mot)[0] def get_enc_current(self, mot): """Get the current encoder value for a motor""" return self.read_value("m%denccurrent" % mot)[0] def set_mode(self, mot, mode): """ Set the mode for a motor. This value is a bit mask, and you can find details about it in the sensor's documentation. """ self.write_value("m%dmode" % mot, (mode,)) def get_mode(self, mot): """ Get the mode for a motor. This value is a bit mask, and you can find details about it in the sensor's documentation. """ return self.read_value("m%dmode" % mot)[0] def set_power(self, mot, power): """Set the power (-100-100) for a motor""" self.write_value("m%dpower" % mot, (power,)) def get_power(self, mot): """Get the power for a motor""" return self.read_value("m%dpower" % mot)[0] def set_gear_ratio(self, mot, ratio): """Set the gear ratio for a motor""" self.write_value("m%dgearratio" % mot, (ratio,)) def get_gear_ratio(self, mot): """Get the gear ratio for a motor""" return self.read_value("m%dgearratio" % mot)[0] def set_pid(self, mot, piddata): """ Set the PID coefficients for a motor. Takes data in MotorCon.PID_Data(p, i, d) format. """ self.write_value("m%dpid" % mot, (piddata.p, piddata.i, piddata.d)) def get_pid(self, mot): """ Get the PID coefficients for a motor. Returns a PID_Data() object. """ p, i, d = self.read_value("m%dpid" % mot) return self.PID_Data(p, i, d) def get_battery_voltage(self): """ Gets the battery voltage (in millivolts/20) """ data = self.read_value("batteryvoltage") high = data[0] low = data[1] return high << 2 | low MotorCon.add_compatible_sensor(None, "HiTechnc", "MotorCon") class Angle(BaseDigitalSensor): """HiTechnic Angle Sensor.""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "mode": (0x41, "c"), "angle": (0x42, "2B"), "angle_acc": (0x44, ">l"), "rpm": (0x48, ">h"), } ) def get_angle(self): v = self.read_value("angle") return v[0] * 2 + v[1] get_sample = get_angle def get_accumulated_angle(self): return self.read_value("angle_acc")[0] def get_rpm(self): return self.read_value("rpm")[0] def calibrate(self): # Current angle will be zero degrees written in EEPROM self.write_value("mode", b"C") def reset(self): # Reset accumulated angle self.write_value("mode", b"R") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736270404.884913 nxt_python-3.5.1/nxt/sensor/mindsensors.py0000644000000000000000000006767414737261105015752 0ustar00# nxt.sensor.mindsensors module -- Classes implementing Mindsensors sensors # Copyright (C) 2006,2007 Douglas P Lau # Copyright (C) 2009 Marcus Wanner, Paulo Vieira, rhn # Copyright (C) 2010 Marcus Wanner, MindSensors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import logging from nxt.sensor import Mode, Type from nxt.sensor.analog import BaseAnalogSensor from nxt.sensor.digital import BaseDigitalSensor logger = logging.getLogger(__name__) class SumoEyes(BaseAnalogSensor): """The class to control Mindsensors Sumo sensor. Warning: long range not working for my sensor. """ # range: 5-10cm class Reading: """Contains the reading of SumoEyes sensor. left and right can be True or False. If True, then there is something there, if False, then it's empty there. """ def __init__(self, raw_reading): self.raw = raw_reading val = raw_reading.normalized_value # FIXME: make it rely on raw_value right = 600 < val < 700 both = 700 <= val < 900 left = 300 < val < 400 self.left = left or both self.right = right or both def __str__(self): return "(left: " + str(self.left) + ", right: " + str(self.right) + ")" def __init__(self, brick, port, long_range=False): super().__init__(brick, port) self.set_long_range(long_range) def set_long_range(self, val): """Sets if the sensor should operate in long range mode (12 inches) or the short range mode (6 in). val should be True or False. """ if val: type_ = Type.LIGHT_INACTIVE else: type_ = Type.LIGHT_ACTIVE self.set_input_mode(type_, Mode.RAW) def get_sample(self): """Returns the processed meaningful values of the sensor""" return self.Reading(self.get_valid_input_values()) class Compassv2(BaseDigitalSensor): """Class for the now-discontinued CMPS-Nx sensor. Also works with v1.1 sensors. Note that when using a v1.x sensor, some of the commands are not supported! To determine your sensor's version, use get_sensor_info().version""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "command": (0x41, "> 4 return str(gs3) + str(gs2) def get_minutes(self): gm = self.read_value("minutes")[0] gm2 = gm & 0xF gm3 = gm & 0x70 gm3 = gm3 >> 4 return str(gm3) + str(gm2) def get_hours(self): gh = self.read_value("hours")[0] gh2 = gh & 0xF gh3 = gh & 0x30 gh3 = gh3 >> 4 return str(gh3) + str(gh2) def get_day(self): gwd = self.read_value("day")[0] gwd = gwd & 0x07 return gwd def get_month(self): gmo = self.read_value("month")[0] gmo2 = gmo & 0xF gmo3 = gmo & 0x10 gmo3 = gmo3 >> 4 return str(gmo3) + str(gmo2) def get_year(self): """Last two digits (10 for 2010)""" gy = self.read_value("year")[0] gy2 = gy & 0xF gy3 = gy & 0xF0 gy3 = gy3 >> 4 return str(gy3) + str(gy2) def get_date(self): gd = self.read_value("date")[0] gd2 = gd & 0xF gd3 = gd & 0x60 gd3 = gd3 >> 4 return str(gd3) + str(gd2) def hour_mode(self, mode): """Writes mode bit and re-enters hours, which is required""" if mode == 12 or 24: hm = self.read_value("hours")[0] hm2 = hm & 0x40 hm2 = hm2 >> 6 if mode == 12 and hm2 == 0: # 12_HOUR = 1 hm3 = hm + 64 self.write_value("hours", (hm3,)) elif mode == 24 and hm2 == 1: # 24_HOUR = 0 hm3 = hm - 64 self.write_value("hours", (hm3,)) else: raise ValueError("Must be 12 or 24!") def get_mer(self): mer = self.read_value("hours")[0] mer2 = mer & 0x40 mer2 = mer2 >> 6 if mer2 == 1: mer3 = mer & 0x20 mer3 = mer3 >> 0x10 return mer3 else: logger.error("cannot get mer in 24-hour mode") def get_sample(self): """Returns a struct_time() tuple which can be processed by the time module.""" import time return time.struct_time( ( int(self.get_year()) + 2000, int(self.get_month()), int(self.get_date()), int(self.get_hours()), int(self.get_minutes()), int(self.get_seconds()), int(self.get_day()), 0, # Should be the Julian Day, but computing that is hard. 0, # No daylight savings time to worry about here. ) ) class ACCL(BaseDigitalSensor): """Class for Accelerometer sensor""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "sensitivity": (0x19, "B"), "command": (0x41, "B"), "x_tilt": (0x42, "b"), "y_tilt": (0x43, "b"), "z_tilt": (0x44, "b"), "all_tilt": (0x42, "3b"), "x_accel": (0x45, "> bit_num return value def get_tasks(self, motor_number): addressname = "tasks_running_m" + str(motor_number) return self.read_value(addressname)[0] def set_pid(self, pid, target, value): addressname = str(pid) + "_" + str(target) self.write_value(addressname, (value,)) def set_pass_count(self, value): self.write_value("pass_count", (value,)) def set_tolerance(self, value): self.write_value("tolerance", (value,)) MMX.add_compatible_sensor(None, "mndsnsrs", "NxTMMX") # Tested with version 'V1.01' class HID(BaseDigitalSensor): """Class for Human Interface Device sensors. These are connected to a computer and look like a keyboard to it.""" I2C_ADDRESS = BaseDigitalSensor.I2C_ADDRESS.copy() I2C_ADDRESS.update( { "command": (0x41, " bool: return (self.value & 0x80) != 0 CODES = { 0x00: None, 0x20: nxt.error.I2CPendingError("pending communication transaction in progress"), 0x40: nxt.error.EmptyMailboxError("specified mailbox queue is empty"), 0x81: nxt.error.SystemProtocolError("no more handles"), 0x82: nxt.error.SystemProtocolError("no space"), 0x83: nxt.error.SystemProtocolError("no more files"), 0x84: nxt.error.SystemProtocolError("end of file expected"), 0x85: nxt.error.SystemProtocolError("end of file"), 0x86: nxt.error.SystemProtocolError("not a linear file"), 0x87: nxt.error.FileNotFoundError("file not found"), 0x88: nxt.error.SystemProtocolError("handle already closed"), 0x89: nxt.error.SystemProtocolError("no linear space"), 0x8A: nxt.error.SystemProtocolError("undefined error"), 0x8B: nxt.error.SystemProtocolError("file is busy"), 0x8C: nxt.error.SystemProtocolError("no write buffers"), 0x8D: nxt.error.SystemProtocolError("append not possible"), 0x8E: nxt.error.SystemProtocolError("file is full"), 0x8F: nxt.error.FileExistsError("file exists"), 0x90: nxt.error.ModuleNotFoundError("module not found"), 0x91: nxt.error.SystemProtocolError("out of bounds"), 0x92: nxt.error.SystemProtocolError("illegal file name"), 0x93: nxt.error.SystemProtocolError("illegal handle"), 0xBD: nxt.error.DirectProtocolError( "request failed (i.e. specified file not found)" ), 0xBE: nxt.error.DirectProtocolError("unknown command opcode"), 0xBF: nxt.error.DirectProtocolError("insane packet"), 0xC0: nxt.error.DirectProtocolError("data contains out-of-range values"), 0xDD: nxt.error.DirectProtocolError("communication bus error"), 0xDE: nxt.error.DirectProtocolError("no free memory in communication buffer"), 0xDF: nxt.error.DirectProtocolError("specified channel/connection is not valid"), 0xE0: nxt.error.I2CError("specified channel/connection not configured or busy"), 0xEC: nxt.error.NoActiveProgramError("no active program"), 0xED: nxt.error.DirectProtocolError("illegal size specified"), 0xEE: nxt.error.DirectProtocolError("illegal mailbox queue ID specified"), 0xEF: nxt.error.DirectProtocolError( "attempted to access invalid field of a structure" ), 0xF0: nxt.error.DirectProtocolError("bad input or output specified"), 0xFB: nxt.error.DirectProtocolError("insufficient memory available"), 0xFF: nxt.error.DirectProtocolError("bad arguments"), } class Telegram: TYPE_DIRECT = 0x00 TYPE_SYSTEM = 0x01 TYPE_REPLY = 0x02 TYPE_REPLY_NOT_REQUIRED = 0x80 def __init__( self, opcode: Opcode, reply_req: bool = True, pkt: Optional[bytes] = None ) -> None: if pkt: self.pkt = BytesIO(pkt) pkt_type = self.parse_u8() if pkt_type != self.TYPE_REPLY: raise nxt.error.ProtocolError("not a reply") pkt_opcode = self.parse_u8() if pkt_opcode != opcode.value: raise nxt.error.ProtocolError(f"invalid reply opcode {pkt_opcode:#02x}") self.opcode = opcode self.reply_req = False else: self.pkt = BytesIO() if opcode.is_system(): typ = self.TYPE_SYSTEM else: typ = self.TYPE_DIRECT if not reply_req: typ |= self.TYPE_REPLY_NOT_REQUIRED self.opcode = opcode self.reply_req = reply_req self.add_u8(typ) self.add_u8(opcode.value) def to_bytes(self) -> bytes: return self.pkt.getvalue() def add_bytes(self, b: bytes) -> None: self.pkt.write(b) def add_string(self, size: int, v: str) -> None: b = v.encode("ascii") if len(b) > size - 1: raise ValueError("string too long") self.pkt.write(pack("%ds" % size, b)) def add_filename(self, fname: str) -> None: self.add_string(20, fname) def add_bool(self, v: bool) -> None: self.pkt.write(pack(" None: self.pkt.write(pack(" None: self.pkt.write(pack(" None: self.pkt.write(pack(" None: self.pkt.write(pack(" bytes: b = self.pkt.read() if size != -1: b = b[:size] return b def parse_string(self, size: int = -1) -> str: b = self.pkt.read(size).rstrip(b"\0") return b.decode("ascii") def parse_filename(self) -> str: return self.parse_string(20) def parse_bool(self) -> bool: return unpack(" int: return unpack(" int: return unpack(" int: return unpack(" int: return unpack(" int: return unpack(" int: return unpack(" None: status = self.parse_u8() if status: ex = CODES.get(status) if ex: raise ex else: raise nxt.error.ProtocolError(f"unknown status code: {status:#02x}") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736675522.160635 nxt_python-3.5.1/pyproject.toml0000644000000000000000000000345414740710302013600 0ustar00[tool.poetry] name = "nxt-python" version = "3.5.1" description = "LEGO Mindstorms NXT Control Package" authors = ["Nicolas Schodet "] license = "GPL-3.0-or-later" readme = "README.md" homepage = "https://sr.ht/~ni/nxt-python/" repository = "https://git.sr.ht/~ni/nxt-python" documentation = "https://ni.srht.site/nxt-python/latest/" packages = [{ include = "nxt" }] include = [ { path = "logo.svg", format = "sdist" }, { path = "tox.ini", format = "sdist" }, { path = "scripts/python-nxt-filer.desktop", format = "sdist" }, { path = "scripts/nxt_*", format = "sdist" }, { path = "scripts/README", format = "sdist" }, { path = "contrib", format = "sdist" }, { path = "docs/Makefile", format = "sdist" }, { path = "docs/conf.py", format = "sdist" }, { path = "docs/favicon.ico", format = "sdist" }, { path = ".pre-commit-config.yaml", format = "sdist" }, { path = "docs/**/*.rst", format = "sdist" }, { path = "examples", format = "sdist" }, { path = "setup.cfg", format = "sdist" }, { path = "tests", format = "sdist" }, ] [tool.poetry.scripts] nxt-push = "nxt.command.push:run" nxt-server = "nxt.command.server:run" nxt-screenshot = "nxt.command.screenshot:run" nxt-test = "nxt.command.test:run" [tool.poetry.dependencies] python = "^3.9" pyusb = "^1.2.1" pybluez = { version = "^0.23", optional = true } pillow = { version = "^9.4.0", optional = true } [tool.poetry.extras] bluetooth = ["pybluez"] screenshot = ["pillow"] [tool.poetry.group.dev.dependencies] pytest = "^7.2.1" coverage = "^6.5.0" pre-commit = "^3.0.4" isort = "^5.12.0" black = "^23.1.0" flake8 = "^5.0.4" mypy = "^1.0.1" sphinx = "^5.3.0" sphinx-rtd-theme = "^1.2.0" tox = "^3.28.0" [tool.poetry.group.nxt-screen.dependencies] pygame = "^2.1" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736597911.3950768 nxt_python-3.5.1/scripts/README0000644000000000000000000000075314740460627013246 0ustar00Are you looking for scripts that were located in this directory? They are now part of the nxt package and generated on installation. If NXT-Python is installed on your system, you can run them by typing their names (nxt-push, nxt-server, nxt-screenshot or nxt-test). If NXT-Python is not installed, the easiest way to run them is by using poetry from somewhere in the source directory like this for example: poetry run nxt-test You can get help with: poetry run nxt-test --help ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704409096.3199499 nxt_python-3.5.1/scripts/nxt_filer0000755000000000000000000003607714545634010014306 0ustar00#!/usr/bin/env python3 # # nxt_filemgr program -- Updated from nxt_filer # Based on: nxt_filer program -- Simple GUI to manage files on a LEGO MINDSTORMS NXT # Copyright (C) 2006 Douglas P Lau # Copyright (C) 2010 rhn # Copyright (C) 2017 TC Wan # Copyright (C) 2018 David Lechner # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import os.path import shutil import sys import urllib.request import gi from usb.core import USBError import nxt.locator from nxt.error import FileExistsError, SystemProtocolError from nxt.locator import BrickNotFoundError gi.require_version("GLib", "2.0") gi.require_version("Gio", "2.0") gi.require_version("Gtk", "3.0") from gi.repository import Gio, GLib, Gtk # noqa: E402 VERSION = "2.0" PROTOCOL_VER = (1, 124) COPYRIGHT = "2006-2018" AUTHORS = ["Douglas P. Lau", "rhn", "TC Wan", "David Lechner"] FILENAME_MINWIDTH = 100 FILENAME_MAXWIDTH = 300 FILENAME_WIDTH = 200 FILESIZE_MINWIDTH = 40 FILESIZE_MAXWIDTH = 120 FILESIZE_WIDTH = 80 FILELIST_HEIGHT = 300 WIN_WIDTH = 800 WIN_HEIGHT = 600 FILELIST_MINWIDTH = 1.0 * WIN_WIDTH FILELIST_MAXWIDTH = 1.0 * WIN_WIDTH def play_soundfile(b, fname): b.play_sound_file(0, fname) def run_program(b, fname): b.start_program(fname) def delete_file(b, fname): b.file_delete(fname) def read_file(b, fname): with b.open_file(fname, "rb") as r: with open(fname, "wb") as f: shutil.copyfileobj(r, f) def file_exists_dialog(win, fname): fileexists_str = "Cannot add %s!" % fname dialog = Gtk.MessageDialog( win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "File Exists" ) dialog.format_secondary_text(fileexists_str) dialog.run() dialog.destroy() def system_error_dialog(win): dialog = Gtk.MessageDialog( win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "System Error" ) dialog.format_secondary_text( "Insufficient Contiguous Space!\n\n" "Please delete files to create contiguous space." ) dialog.run() dialog.destroy() def usb_error_dialog(win): dialog = Gtk.MessageDialog( win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "USB Error" ) dialog.format_secondary_text("Can't connect to NXT!\n\nIs the NXT powered off?") dialog.run() dialog.destroy() def write_file(win, b, fname, data): try: with b.open_file(fname, "wb", len(data)) as w: w.write(data) except FileExistsError: file_exists_dialog(win, fname) except SystemProtocolError: system_error_dialog(win) except USBError: raise def write_files(win, b, names): for fname in names.split("\r\n"): if fname: bname = os.path.basename(fname) url = urllib.request.urlopen(fname) try: data = url.read() finally: url.close() write_file(win, b, bname, data) class NXTListing(Gtk.ListStore): def __init__(self): "Create an empty file list" Gtk.ListStore.__init__(self, str, str) self.set_sort_column_id(0, Gtk.SortType(Gtk.SortType.ASCENDING)) def populate(self, brick, pattern): for fname, size in brick.find_files(pattern): self.append((fname, str(size))) class ApplicationWindow(Gtk.ApplicationWindow): # FIXME # TARGETS = Gtk.target_list_add_uri_targets() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_default_size(WIN_WIDTH, WIN_HEIGHT) self.set_resizable(False) self.set_border_width(6) self.brick = None self.selected_file = None self.brick_info_str = "Disconnected" self.nxt_filelist = NXTListing() self.status_bar = Gtk.Statusbar() self.connect_status_id = self.status_bar.get_context_id("connect") self.connect_msg_id = None h = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6) self.brick_button = Gtk.Button(label="Connect") self.brick_button.connect("clicked", self.reload_filelist) h.pack_start(self.brick_button, False, True, 0) h.pack_start(self.make_file_panel(), True, True, 0) h.pack_start(self.make_button_panel(), False, True, 0) h.pack_end(self.status_bar, False, True, 0) self.add(h) self.reload_filelist(self.nxt_filelist) self.show_all() def make_file_view(self): tv = Gtk.TreeView() tv.set_headers_visible(True) tv.set_property("fixed_height_mode", True) r = Gtk.CellRendererText() c = Gtk.TreeViewColumn("File name", r, text=0) c.set_fixed_width(FILENAME_WIDTH) c.set_min_width(FILENAME_MINWIDTH) c.set_max_width(FILENAME_MAXWIDTH) c.set_resizable(True) c.set_sizing(Gtk.TreeViewColumnSizing.FIXED) tv.append_column(c) r = Gtk.CellRendererText() c = Gtk.TreeViewColumn("Bytes", r, text=1) c.set_resizable(True) c.set_fixed_width(FILESIZE_WIDTH) c.set_min_width(FILESIZE_MINWIDTH) c.set_max_width(FILESIZE_MAXWIDTH) c.set_sizing(Gtk.TreeViewColumnSizing.FIXED) tv.append_column(c) # FIXME # tv.enable_model_drag_source(Gtk.gdk.BUTTON1_MASK, self.TARGETS, # Gtk.gdk.ACTION_DEFAULT | Gtk.gdk.ACTION_MOVE) # tv.enable_model_drag_dest(self.TARGETS, Gtk.gdk.ACTION_COPY) # tv.connect("drag_data_get", self.drag_data_get_data) tv.connect("drag_data_received", self.drag_data_received_data) tv.connect("row_activated", self.row_activated_action) return tv def make_file_panel(self): v = Gtk.Box(homogeneous=Gtk.Orientation.VERTICAL, spacing=0) tv = self.make_file_view() tv.set_model(self.nxt_filelist) select = tv.get_selection() select.connect("changed", self.on_tree_selection_changed) s = Gtk.ScrolledWindow() s.set_min_content_width(FILELIST_MINWIDTH) s.set_min_content_height(FILELIST_HEIGHT) s.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) s.add(tv) s.set_border_width(2) v.pack_start(s, True, False, 0) return v def make_button_panel(self): vb = Gtk.Box(homogeneous=Gtk.Orientation.HORIZONTAL, spacing=0) self.delete_button = Gtk.Button(label="Remove") self.delete_button.connect("clicked", self.remove_file) vb.pack_start(self.delete_button, True, True, 10) self.add_button = Gtk.Button(label="Add") self.add_button.connect("clicked", self.add_file) vb.pack_start(self.add_button, True, True, 10) self.exec_button = Gtk.Button(label="Execute") self.exec_button.connect("clicked", self.execute_file) vb.pack_start(self.exec_button, True, True, 10) return vb def do_housekeeping(self): self.brick.close() self.brick = None self.selected_file = None def reload_filelist(self, widget): if self.brick: self.do_housekeeping() try: self.brick = nxt.locator.find() except BrickNotFoundError: print("Brick not found!") except USBError: usb_error_dialog(self) self.do_housekeeping() if self.brick: self.update_view() def update_view(self): self.get_brick_info() if self.connect_msg_id: self.status_bar.remove(self.connect_status_id, self.connect_msg_id) self.connect_msg_id = self.status_bar.push( self.connect_status_id, self.brick_info_str ) self.nxt_filelist.clear() if self.brick: self.nxt_filelist.populate(self.brick, "*.*") def get_brick_info(self): if self.brick: print("Reading from NXT..."), prot_version, fw_version = self.brick.get_firmware_version() print("Protocol version: %s.%s" % prot_version) if prot_version == PROTOCOL_VER: ( brick_name, brick_hostid, brick_signal_strengths, brick_user_flash, ) = self.brick.get_device_info() connection_type = str(self.brick._sock) self.brick_info_str = ( "Connected via %s to %s [%s]\tFirmware: v%s.%s\tFree space: %s" % ( connection_type, brick_name, brick_hostid, fw_version[0], fw_version[1], brick_user_flash, ) ) else: print("Invalid Protocol version! Closing connection.") self.do_housekeeping() else: self.brick_info_str = "Disconnected" sys.stdout.flush() def choose_files(self): dialog = Gtk.FileChooserNative() dialog.new("Add File", self, Gtk.FileChooserAction.OPEN, None, None) dialog.set_transient_for(self) dialog.set_select_multiple(False) res = dialog.run() if res == Gtk.ResponseType.ACCEPT: # Handle single file download for now uri = dialog.get_uri() try: write_files(self, self.brick, uri) except USBError: # Cannot recover, close connection usb_error_dialog(self) self.do_housekeeping() dialog.destroy() def on_tree_selection_changed(self, selection): model, treeiter = selection.get_selected() if treeiter: self.selected_file = model[treeiter][0] else: self.selected_file = None def drag_data_get_data(self, treeview, context, selection, target_id, etime): treeselection = treeview.get_selection() model, iter = treeselection.get_selected() data = model.get_value(iter, 0) print(data) selection.set(selection.target, 8, data) def drag_data_received_data(self, treeview, context, x, y, selection, info, etime): if context.action == Gtk.gdk.ACTION_COPY: write_files(self, self.brick, selection.data) # FIXME: update file listing after writing files # FIXME: finish context def row_activated_action(self, treeview, context, selection): self.execute_file(selection) def execute_file(self, widget): if self.selected_file: name = "" ext = "" try: name, ext = self.selected_file.split(".") if ext == "rso": play_soundfile(self.brick, self.selected_file) elif ext == "rxe": run_program(self.brick, self.selected_file) self.do_housekeeping() self.update_view() else: print("Can't execute '*.%s' files" % ext) except ValueError: print("No file extension (unknown file type)") except USBError: # Cannot recover, close connection usb_error_dialog(self) self.do_housekeeping() self.update_view() def remove_file(self, widget): if self.selected_file: warning_str = "Do you really want to remove %s?" % self.selected_file dialog = Gtk.MessageDialog( self, 0, Gtk.MessageType.WARNING, Gtk.ButtonsType.OK_CANCEL, "Remove File", ) dialog.format_secondary_text(warning_str) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.OK: try: delete_file(self.brick, self.selected_file) except USBError: # Cannot recover, close connection usb_error_dialog(self) self.do_housekeeping() self.selected_file = None self.update_view() def add_file(self, widget): if self.brick: self.choose_files() self.selected_file = None self.update_view() class AboutDialog(Gtk.AboutDialog): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_version(VERSION) self.set_copyright(COPYRIGHT) self.set_comments("LEGO® MINDSTORMS NXT File Manager") self.set_license("Released under GPL v2 or later") self.set_website("https://sr.ht/~ni/nxt-python/") self.set_website_label("Wiki") self.set_authors(AUTHORS) markup = "{}".format( "LEGO® is a trademark of the LEGO Group of companies which does not " "sponsor, authorize or endorse this software." ) disclaimer = Gtk.Label() disclaimer.set_line_wrap(True) disclaimer.set_max_width_chars(10) disclaimer.set_markup(markup) disclaimer.show() self.get_content_area().pack_end(disclaimer, False, False, 6) def do_response(self, response): self.close() class Application(Gtk.Application): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.window = None self.set_option_context_summary( "Simple GUI to manage files on a LEGO MINDSTORMS NXT" ) def do_startup(self): Gtk.Application.do_startup(self) action = Gio.SimpleAction.new("about", None) action.connect("activate", self.on_about) self.add_action(action) action = Gio.SimpleAction.new("quit", None) action.connect("activate", self.on_quit) self.add_action(action) # on macOS, the menu is created automatically, but still uses the # actions above if app.prefers_app_menu(): menu1 = Gio.Menu.new() menu1.append("_About", "app.about") menu2 = Gio.Menu.new() menu2.append("_Quit", "app.quit") app.set_accels_for_action("app.quit", ["q"]) appMenu = Gio.Menu.new() appMenu.append_section(None, menu1) appMenu.append_section(None, menu2) app.set_app_menu(appMenu) def do_activate(self): if not self.window: self.window = ApplicationWindow(application=self) self.window.present() def on_about(self, action, param): about_dialog = AboutDialog(transient_for=self.window, modal=True) about_dialog.present() def on_quit(self, action, param): self.quit() if __name__ == "__main__": GLib.set_prgname("nxt_filer") GLib.set_application_name("NXT File Manager") app = Application() app.run(sys.argv) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1638907410.063749 nxt_python-3.5.1/scripts/python-nxt-filer.desktop0000644000000000000000000000027014153737022017174 0ustar00[Desktop Entry] Encoding=UTF-8 Name=NXT filer Comment=Lego Mindstorms NXT file viewer Exec=/usr/bin/nxt_filer Terminal=false Type=Application Categories=Utility;FileTools;Electronics; ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734478811.0219114 nxt_python-3.5.1/setup.cfg0000644000000000000000000000051314730405733012506 0ustar00[flake8] max-line-length = 88 [isort] profile = black [tool:pytest] addopts = --doctest-modules testpaths = nxt tests [coverage:run] command_line = -m pytest source = nxt [check-manifest] ignore = .builds/* [mypy] packages = nxt [mypy-usb.*] ignore_missing_imports = True [mypy-bluetooth.*] ignore_missing_imports = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735691247.2269268 nxt_python-3.5.1/tests/conftest.py0000644000000000000000000000572714735105757014252 0ustar00# conftest -- Common test fixtures # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import Mock, patch import pytest import nxt.brick def pytest_addoption(parser): parser.addoption( "--run-nxt", action="append", choices=("usb", "bluetooth", "devfile", "socket"), default=[], help="run tests needing a real NXT connected over given interface", ) def pytest_configure(config): config.addinivalue_line("markers", "nxt: mark test as needing a real NXT") def pytest_collection_modifyitems(config, items): run_nxt = config.getoption("--run-nxt") for item in items: for marker in item.iter_markers("nxt"): if marker.args[0] not in run_nxt: item.add_marker( pytest.mark.skip( reason="need --run-nxt=%s option to run" % marker.args[0] ) ) @pytest.fixture def mtime(): """Mock time.time() and time.sleep().""" current_time = 0 def timef(): nonlocal current_time return current_time def sleepf(delay): nonlocal current_time current_time += delay mtime = Mock(spec_set=("time", "sleep")) mtime.time.side_effect = timef mtime.sleep.side_effect = sleepf with ( patch("nxt.brick.time", new=mtime), patch("nxt.motcont.time", new=mtime), patch("nxt.motor.time", new=mtime), patch("nxt.sensor.analog.time", new=mtime), patch("nxt.sensor.digital.time", new=mtime), ): yield mtime def make_brick_mock(): b = Mock() def find_files(pattern): return nxt.brick.Brick.find_files(b, pattern) def find_modules(pattern): return nxt.brick.Brick.find_modules(b, pattern) def open_file(*args, **kwargs): return nxt.brick.Brick.open_file(b, *args, **kwargs) def get_sensor(*args, **kwargs): return nxt.brick.Brick.get_sensor(b, *args, **kwargs) def get_motor(*args, **kwargs): return nxt.brick.Brick.get_motor(b, *args, **kwargs) b._sock.bsize = 60 b._sock.type = "usb" b.find_files = find_files b.find_modules = find_modules b.open_file = open_file b.get_sensor = get_sensor b.get_motor = get_motor return b @pytest.fixture def mbrick(mtime): """A brick with mocked low level functions.""" return make_brick_mock() @pytest.fixture def mbrick2(mtime): """A second brick with mocked low level functions.""" return make_brick_mock() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/tests/test_backend_bluetooth.py0000644000000000000000000001406314155725071017122 0ustar00# test_backend_bluetooth -- Test nxt.backend.bluetooth module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import Mock, call, patch import pytest import nxt.backend.bluetooth @pytest.fixture def msock(): sock = Mock( spec_set=( "connect", "close", "send", "recv", ) ) return sock @pytest.fixture def mbluetooth(msock): class BluetoothError(Exception): pass bluetooth = Mock() bluetooth.BluetoothSocket.return_value = msock bluetooth.BluetoothError = BluetoothError return bluetooth @pytest.fixture def mbluetooth_import(mbluetooth): orig_import = __import__ def mocked_import(name, *args): if name == "bluetooth": return mbluetooth return orig_import(name, *args) with patch("builtins.__import__", new=mocked_import): yield mocked_import @pytest.fixture def mbluetooth_import_error(): orig_import = __import__ def mocked_import(name, *args): if name == "bluetooth": raise ImportError("mocked") return orig_import(name, *args) with patch("builtins.__import__", new=mocked_import): yield mocked_import @pytest.fixture def mbluetooth_import_not_supported(): orig_import = __import__ def mocked_import(name, *args): if name == "bluetooth": raise Exception("mocked") return orig_import(name, *args) with patch("builtins.__import__", new=mocked_import): yield mocked_import def test_bluetooth(mbluetooth, mbluetooth_import, msock): # Instantiate backend. backend = nxt.backend.bluetooth.get_backend() # Find brick. mbluetooth.discover_devices.return_value = [ "00:01:02:03:04:05", ] bricks = list(backend.find(blah="blah")) assert len(bricks) == 1 brick = bricks[0] assert mbluetooth.BluetoothSocket.called assert msock.connect.called sock = brick._sock # str. assert str(sock) == "Bluetooth (00:01:02:03:04:05)" # Send. some_bytes = bytes.fromhex("01020304") some_len = bytes.fromhex("0400") some_bytes_with_len = some_len + some_bytes sock.send(some_bytes) assert msock.send.call_args == call(some_bytes_with_len) # Recv. msock.recv.side_effect = [some_len, some_bytes] r = sock.recv() assert r == some_bytes assert msock.recv.called # Close. brick.close() assert msock.close.called # Duplicated close. sock.close() def test_bluetooth_by_name(mbluetooth, mbluetooth_import, msock): # Instantiate backend. backend = nxt.backend.bluetooth.get_backend() # Find brick. mbluetooth.discover_devices.return_value = [ ("00:01:02:03:04:05", "NXT"), ("00:01:02:03:04:06", "NXT"), ("00:01:02:03:04:07", "NXT2"), ] bricks = list(backend.find(name="NXT2", blah="blah")) assert len(bricks) == 1 brick = bricks[0] assert mbluetooth.BluetoothSocket.called assert msock.connect.called sock = brick._sock # str. assert str(sock) == "Bluetooth (00:01:02:03:04:07)" # Close. brick.close() def test_bluetooth_by_host(mbluetooth, mbluetooth_import, msock): # Instantiate backend. backend = nxt.backend.bluetooth.get_backend() # Find brick. bricks = list(backend.find(host="00:01:02:03:04:05", blah="blah")) assert len(bricks) == 1 brick = bricks[0] assert not mbluetooth.discover_devices.called assert mbluetooth.BluetoothSocket.called assert msock.connect.called sock = brick._sock # str. assert str(sock) == "Bluetooth (00:01:02:03:04:05)" # Close. brick.close() def test_bluetooth_by_host_and_name(mbluetooth, mbluetooth_import, msock): # Instantiate backend. backend = nxt.backend.bluetooth.get_backend() # Find brick. bricks = list(backend.find(host="00:01:02:03:04:05", name="NXT")) assert len(bricks) == 1 brick = bricks[0] assert not mbluetooth.discover_devices.called assert mbluetooth.BluetoothSocket.called assert msock.connect.called sock = brick._sock # str. assert str(sock) == "Bluetooth (00:01:02:03:04:05)" # Close. brick.close() def test_bluetooth_not_present(mbluetooth_import_error): assert nxt.backend.bluetooth.get_backend() is None def test_bluetooth_not_supported(mbluetooth_import_not_supported): assert nxt.backend.bluetooth.get_backend() is None def test_bluetooth_cant_connect(mbluetooth, mbluetooth_import, msock): backend = nxt.backend.bluetooth.get_backend() mbluetooth.discover_devices.return_value = [ "00:01:02:03:04:05", ] msock.connect.side_effect = [mbluetooth.BluetoothError] bricks = list(backend.find(blah="blah")) assert len(bricks) == 0 def test_bluetooth_cant_discover(mbluetooth, mbluetooth_import, msock): backend = nxt.backend.bluetooth.get_backend() mbluetooth.discover_devices.side_effect = [OSError] bricks = list(backend.find(blah="blah")) assert len(bricks) == 0 mbluetooth.discover_devices.side_effect = [mbluetooth.BluetoothError] bricks = list(backend.find(blah="blah")) assert len(bricks) == 0 @pytest.mark.nxt("bluetooth") def test_bluetooth_real(): # Instantiate backend. backend = nxt.backend.bluetooth.get_backend() # Find brick. bricks = list(backend.find()) assert len(bricks) > 0, "no NXT found" brick = bricks[0] sock = brick._sock # str. assert str(sock).startswith("Bluetooth (") # Send. sock.send(bytes.fromhex("019b")) # Recv. r = sock.recv() assert r.startswith(bytes.fromhex("029b00")) # Close. brick.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/tests/test_backend_devfile.py0000644000000000000000000000743114155725071016534 0ustar00# test_backend_devfile -- Test nxt.backend.devfile module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import Mock, call, patch import pytest import nxt.backend.devfile @pytest.fixture def mdev(): dev = Mock( spec_set=( "read", "write", "close", ) ) return dev @pytest.fixture def mopen(mdev): with patch("nxt.backend.devfile.open") as fopen: fopen.return_value = mdev yield fopen @pytest.fixture def mglob(): with patch("nxt.backend.devfile.glob") as glob: yield glob @pytest.fixture def mtty(): with patch("nxt.backend.devfile.tty") as tty: yield tty @pytest.fixture def mplatform(): with patch("nxt.backend.devfile.platform") as platform: yield platform def test_devfile(mopen, mtty, mdev): # Instantiate backend. backend = nxt.backend.devfile.get_backend() # Find brick. bricks = list(backend.find(filename="/dev/nxt", blah="blah")) assert len(bricks) == 1 brick = bricks[0] assert mopen.called assert mtty.setraw.called sock = brick._sock # str. assert str(sock).startswith("DevFile (/dev") # Send. some_bytes = bytes.fromhex("01020304") some_len = bytes.fromhex("0400") some_bytes_with_len = some_len + some_bytes sock.send(some_bytes) assert mdev.write.call_args == call(some_bytes_with_len) # Recv. mdev.read.side_effect = [some_len, some_bytes] r = sock.recv() assert r == some_bytes assert mdev.read.called # Close. brick.close() assert mdev.close.called # Duplicated close. sock.close() def test_devfile_linux(mopen, mtty, mglob, mplatform): mplatform.system.return_value = "Linux" mglob.glob.return_value = ["/dev/rfcomm0"] backend = nxt.backend.devfile.get_backend() bricks = list(backend.find(blah="blah")) assert len(bricks) == 1 assert mglob.mock_calls == [call.glob("/dev/rfcomm*")] def test_devfile_darwin(mopen, mtty, mglob, mplatform): mplatform.system.return_value = "Darwin" mglob.glob.return_value = ["/dev/tty.NXT-DevB-1"] backend = nxt.backend.devfile.get_backend() bricks = list(backend.find(name="NXT", blah="blah")) assert len(bricks) == 1 bricks = list(backend.find(blah="blah")) assert len(bricks) == 1 assert mglob.mock_calls == [ call.glob("/dev/*NXT*"), call.glob("/dev/*-DevB*"), ] def test_devfile_other(mplatform): mplatform.system.return_value = "MSDos" backend = nxt.backend.devfile.get_backend() bricks = list(backend.find(blah="blah")) assert len(bricks) == 0 def test_devfile_cant_connect(mopen, mtty, mdev): backend = nxt.backend.devfile.get_backend() mopen.side_effect = [OSError] bricks = list(backend.find(filename="/dev/nxt", blah="blah")) assert len(bricks) == 0 @pytest.mark.nxt("devfile") def test_devfile_real(): # Instantiate backend. backend = nxt.backend.devfile.get_backend() # Find brick. bricks = list(backend.find()) assert len(bricks) > 0, "no NXT found" brick = bricks[0] sock = brick._sock # str. assert str(sock).startswith("DevFile (/dev") # Send. sock.send(bytes.fromhex("019b")) # Recv. r = sock.recv() assert r.startswith(bytes.fromhex("029b00")) # Close. brick.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/tests/test_backend_socket.py0000644000000000000000000000507514155725071016410 0ustar00# test_backend_socket -- Test nxt.backend.socket module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import Mock, call, patch import pytest import nxt.backend.socket @pytest.fixture def mdev(): dev = Mock( spec_set=( "connect", "send", "recv", "close", ) ) return dev @pytest.fixture def msocket(mdev): with patch("nxt.backend.socket.socket") as socket: socket.socket.return_value = mdev yield socket def test_socket(msocket, mdev): # Instantiate backend. backend = nxt.backend.socket.get_backend() # Find brick. mdev.recv.return_value = b"usb" bricks = list(backend.find(blah="blah")) assert len(bricks) == 1 brick = bricks[0] assert brick._sock.type == "ipusb" assert msocket.socket.called assert mdev.connect.called assert mdev.send.call_args == call(b"\x98") sock = brick._sock # str. assert str(sock) == "Socket (localhost:2727)" # Send. some_bytes = bytes.fromhex("01020304") sock.send(some_bytes) assert mdev.send.call_args == call(some_bytes) # Recv. mdev.recv.return_value = some_bytes r = sock.recv() assert r == some_bytes assert mdev.recv.called # Close. brick.close() assert mdev.send.call_args == call(b"\x99") assert mdev.close.called # Duplicated close. sock.close() def test_socket_cant_connect(msocket, mdev): backend = nxt.backend.socket.get_backend() mdev.connect.side_effect = [ConnectionRefusedError] bricks = list(backend.find(blah="blah")) assert len(bricks) == 0 @pytest.mark.nxt("socket") def test_socket_real(): # Instantiate backend. backend = nxt.backend.socket.get_backend() # Find brick. bricks = list(backend.find()) assert len(bricks) > 0, "no NXT found" brick = bricks[0] sock = brick._sock # str. assert str(sock) == "Socket (localhost:2727)" # Send. sock.send(bytes.fromhex("019b")) # Recv. r = sock.recv() assert r.startswith(bytes.fromhex("029b00")) # Close. brick.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/tests/test_backend_usb.py0000644000000000000000000000466514155725071015715 0ustar00# test_backend_usb -- Test nxt.backend.usb module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import array from unittest.mock import Mock, call, patch import pytest import nxt.backend.usb @pytest.fixture def mdev(): dev = Mock( spec_set=( "reset", "set_configuration", "get_active_configuration", "bus", "address", ) ) dev.bus = 1 dev.address = 2 return dev @pytest.fixture def musb(mdev): with patch("nxt.backend.usb.usb.core") as usb_core: usb_core.find.return_value = [mdev] yield usb_core def test_usb(musb, mdev): # Instantiate backend. backend = nxt.backend.usb.get_backend() # Find brick. epout = Mock(spec_set=("write",)) epin = Mock(spec_set=("read",)) mdev.get_active_configuration.return_value = {(0, 0): (epout, epin)} bricks = list(backend.find(blah="blah")) assert len(bricks) == 1 brick = bricks[0] assert mdev.reset.called assert mdev.set_configuration.called assert mdev.get_active_configuration.called sock = brick._sock # str. assert str(sock).startswith("USB (Bus") # Send. some_bytes = bytes.fromhex("01020304") sock.send(some_bytes) assert epout.write.call_args == call(some_bytes) # Recv. epin.read.return_value = array.array("B", (1, 2, 3, 4)) r = sock.recv() assert r == some_bytes assert epin.read.called # Close. brick.close() # Duplicated close. sock.close() @pytest.mark.nxt("usb") def test_usb_real(): # Instantiate backend. backend = nxt.backend.usb.get_backend() # Find brick. bricks = list(backend.find()) assert len(bricks) > 0, "no NXT found" brick = bricks[0] sock = brick._sock # str. assert str(sock).startswith("USB (Bus") # Send. sock.send(bytes.fromhex("019b")) # Recv. r = sock.recv() assert r.startswith(bytes.fromhex("029b00")) # Close. brick.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735655419.6177082 nxt_python-3.5.1/tests/test_brick.py0000644000000000000000000006527114734777774014573 0ustar00# test_brick -- Test nxt.brick module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import Mock, call, patch import pytest import nxt.brick import nxt.error import nxt.motor @pytest.fixture def sock(): return Mock(spec_set=("send", "recv", "close")) @pytest.fixture def brick(sock): """A brick with a mock socket.""" return nxt.brick.Brick(sock) def sent(sent): return [call.send(sent)] def sent_recved(sent): return [call.send(sent), call.recv()] test_rxe_bin = b"test.rxe\0\0\0\0\0\0\0\0\0\0\0\0" test_rso_bin = b"test.rso\0\0\0\0\0\0\0\0\0\0\0\0" star_star_bin = b"*.*\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" loader_bin = b"Loader\0\0\0\0\0\0\0\0\0\0\0\0\0\0" def test_close(sock, brick): brick.close() assert sock.mock_calls == [call.close()] def test_with(sock, brick): with brick: pass assert sock.mock_calls == [call.close()] def test_reply_error(sock, brick): sock.recv.return_value = bytes.fromhex("019700") + b"No\0" with pytest.raises(nxt.error.ProtocolError): brick.boot(sure=True) sock.recv.return_value = bytes.fromhex("029900") + b"No\0" with pytest.raises(nxt.error.ProtocolError): brick.boot(sure=True) sock.recv.return_value = bytes.fromhex("029799") + b"No\0" with pytest.raises(nxt.error.ProtocolError): brick.boot(sure=True) class TestSystem: """Test system commands.""" def test_file_open_read(self, sock, brick): sock.recv.return_value = bytes.fromhex("028000 42 01020304") handle, size = brick.file_open_read("test.rxe") assert sock.mock_calls == sent_recved(bytes.fromhex("0180") + test_rxe_bin) assert handle == 0x42 assert size == 0x04030201 def test_file_open_read_fail(self, sock, brick): """Test command failure reported in status code.""" sock.recv.return_value = bytes.fromhex("028087 00 00000000") with pytest.raises(nxt.error.FileNotFoundError): brick.file_open_read("unknown.rxe") def test_file_open_write(self, sock, brick): sock.recv.return_value = bytes.fromhex("028100 42") handle = brick.file_open_write("test.rxe", 0x04030201) assert sock.mock_calls == sent_recved( bytes.fromhex("0181") + test_rxe_bin + bytes.fromhex("01020304") ) assert handle == 0x42 def test_file_read(self, sock, brick): sock.recv.return_value = bytes.fromhex("028200 42 0700 21222324252627") handle, data = brick.file_read(0x42, 7) # TODO: The file_read command will return a EOF error if end of file is reached # before the read can be finished, and size will still contain the requested # data length instead of the actual read length. This is not the classic # semantic of a Unix read. Avoid reading past the end of file. assert sock.mock_calls == sent_recved(bytes.fromhex("0182 42 0700")) assert handle == 0x42 assert data == bytes.fromhex("21222324252627") def test_file_write(self, sock, brick): sock.recv.return_value = bytes.fromhex("028300 42 0700") handle, size = brick.file_write(0x42, bytes.fromhex("21222324252627")) assert sock.mock_calls == sent_recved(bytes.fromhex("0183 42 21222324252627")) assert handle == 0x42 assert size == 7 def test_file_close(self, sock, brick): sock.recv.return_value = bytes.fromhex("028400 42") handle = brick.file_close(0x42) assert sock.mock_calls == sent_recved(bytes.fromhex("0184 42")) assert handle == 0x42 def test_file_delete(self, sock, brick): sock.recv.return_value = ( bytes.fromhex("028500") + b"test.rxe\0\0\0\0\0\0\0\0\0\0\0\0" ) name = brick.file_delete("test.rxe") assert sock.mock_calls == sent_recved( bytes.fromhex("0185") + b"test.rxe\0\0\0\0\0\0\0\0\0\0\0\0" ) assert name == "test.rxe" def test_file_find_first(self, sock, brick): sock.recv.return_value = ( bytes.fromhex("028600 42") + test_rxe_bin + bytes.fromhex("01020304") ) handle, name, size = brick.file_find_first("*.*") assert sock.mock_calls == sent_recved(bytes.fromhex("0186") + star_star_bin) assert handle == 0x42 assert name == "test.rxe" assert size == 0x04030201 def test_file_find_next(self, sock, brick): sock.recv.return_value = ( bytes.fromhex("028700 42") + test_rxe_bin + bytes.fromhex("01020304") ) handle, name, size = brick.file_find_next(0x42) assert sock.mock_calls == sent_recved(bytes.fromhex("0187 42")) assert handle == 0x42 assert name == "test.rxe" assert size == 0x04030201 def test_get_firmware_version(self, sock, brick): sock.recv.return_value = bytes.fromhex("028800 0102 0304") prot_version, fw_version = brick.get_firmware_version() assert sock.mock_calls == sent_recved(bytes.fromhex("0188")) assert prot_version == (2, 1) assert fw_version == (4, 3) def test_file_open_write_linear(self, sock, brick): sock.recv.return_value = bytes.fromhex("028900 42") handle = brick.file_open_write_linear("test.rxe", 0x04030201) assert sock.mock_calls == sent_recved( bytes.fromhex("0189") + test_rxe_bin + bytes.fromhex("01020304") ) assert handle == 0x42 def test_file_open_write_data(self, sock, brick): sock.recv.return_value = bytes.fromhex("028b00 42") handle = brick.file_open_write_data("test.rxe", 0x04030201) assert sock.mock_calls == sent_recved( bytes.fromhex("018b") + test_rxe_bin + bytes.fromhex("01020304") ) assert handle == 0x42 def test_file_open_append_data(self, sock, brick): sock.recv.return_value = bytes.fromhex("028c00 42 01020304") handle, available_size = brick.file_open_append_data("test.rxe") assert sock.mock_calls == sent_recved(bytes.fromhex("018c") + test_rxe_bin) assert handle == 0x42 assert available_size == 0x04030201 def test_module_find_first(self, sock, brick): sock.recv.return_value = ( bytes.fromhex("029000 42") + loader_bin + bytes.fromhex("01000900 00000000 0800") ) handle, name, mod_id, mod_size, mod_iomap_size = brick.module_find_first("*.*") assert sock.mock_calls == sent_recved(bytes.fromhex("0190") + star_star_bin) assert handle == 0x42 assert name == "Loader" assert mod_id == 0x00090001 assert mod_size == 0 assert mod_iomap_size == 8 def test_module_find_next(self, sock, brick): sock.recv.return_value = ( bytes.fromhex("029100 42") + loader_bin + bytes.fromhex("01000900 00000000 0800") ) handle, name, mod_id, mod_size, mod_iomap_size = brick.module_find_next(0x42) assert sock.mock_calls == sent_recved(bytes.fromhex("0191 42")) assert handle == 0x42 assert name == "Loader" assert mod_id == 0x00090001 assert mod_size == 0 assert mod_iomap_size == 8 def test_module_close(self, sock, brick): sock.recv.return_value = bytes.fromhex("029200 42") handle = brick.module_close(0x42) assert sock.mock_calls == sent_recved(bytes.fromhex("0192 42")) assert handle == 0x42 def test_read_io_map(self, sock, brick): sock.recv.return_value = bytes.fromhex( "029400 01020304 0700 212223242526270000" ) mod_id, data = brick.read_io_map(0x04030201, 0x3231, 0x4241) assert sock.mock_calls == sent_recved(bytes.fromhex("0194 01020304 3132 4142")) assert mod_id == 0x04030201 assert data == bytes.fromhex("21222324252627") def test_write_io_map(self, sock, brick): sock.recv.return_value = bytes.fromhex("029500 01020304 0700") mod_id, size = brick.write_io_map( 0x04030201, 0x3231, bytes.fromhex("21222324252627") ) assert sock.mock_calls == sent_recved( bytes.fromhex("0195 01020304 3132 0700 21222324252627") ) assert mod_id == 0x04030201 assert size == 7 def test_boot_not_sure(self, sock, brick): with pytest.raises(ValueError): brick.boot() def test_boot(self, sock, brick): sock.recv.return_value = bytes.fromhex("029700") + b"Yes\0" resp = brick.boot(sure=True) assert sock.mock_calls == sent_recved( bytes.fromhex("0197") + b"Let's dance: SAMBA\0" ) assert resp == b"Yes\0" def test_set_brick_name(self, sock, brick): sock.recv.return_value = bytes.fromhex("029800") brick.set_brick_name("NXT") assert sock.mock_calls == sent_recved( bytes.fromhex("0198") + b"NXT\0\0\0\0\0\0\0\0\0\0\0\0" ) def test_set_brick_name_too_long(self, sock, brick): with pytest.raises(ValueError): brick.set_brick_name("NXT456789012345") def test_get_device_info(self, sock, brick): sock.recv.return_value = ( bytes.fromhex("029b00") + b"NXT\0\0\0\0\0\0\0\0\0\0\0\0" + bytes.fromhex("01020304050600" "11121314" "21222324") ) name, address, signal_strengths, user_flash = brick.get_device_info() assert sock.mock_calls == sent_recved(bytes.fromhex("019b")) assert name == "NXT" assert address == "01:02:03:04:05:06" assert signal_strengths == (0x11, 0x12, 0x13, 0x14) assert user_flash == 0x24232221 def test_delete_user_flash(self, sock, brick): sock.recv.return_value = bytes.fromhex("02a000") brick.delete_user_flash() assert sock.mock_calls == sent_recved(bytes.fromhex("01a0")) def test_poll_command_length(self, sock, brick): sock.recv.return_value = bytes.fromhex("02a100 01 07") buf_num, size = brick.poll_command_length(1) assert sock.mock_calls == sent_recved(bytes.fromhex("01a1 01")) assert buf_num == 1 assert size == 7 def test_poll_command(self, sock, brick): sock.recv.return_value = bytes.fromhex("02a200 01 07 212223242526270000") buf_num, command = brick.poll_command(1, 9) assert sock.mock_calls == sent_recved(bytes.fromhex("01a2 01 09")) assert buf_num == 1 assert command == bytes.fromhex("21222324252627") def test_bluetooth_factory_reset(self, sock, brick): sock.recv.return_value = bytes.fromhex("02a400") brick.bluetooth_factory_reset() assert sock.mock_calls == sent_recved(bytes.fromhex("01a4")) class TestDirect: """Test direct commands.""" def test_start_program(self, sock, brick): sock.recv.return_value = bytes.fromhex("020000") brick.start_program("test.rxe") assert sock.mock_calls == sent_recved(bytes.fromhex("0000") + test_rxe_bin) def test_stop_program(self, sock, brick): sock.recv.return_value = bytes.fromhex("020100") brick.stop_program() assert sock.mock_calls == sent_recved(bytes.fromhex("0001")) def test_stop_program_fail(self, sock, brick): sock.recv.return_value = bytes.fromhex("0201ec") with pytest.raises(nxt.error.NoActiveProgramError): brick.stop_program() def test_play_sound_file(self, sock, brick): brick.play_sound_file(True, "test.rso") assert sock.mock_calls == sent(bytes.fromhex("8002 01") + test_rso_bin) def test_play_tone(self, sock, brick): brick.play_tone(440, 1000) assert sock.mock_calls == sent(bytes.fromhex("8003 b801 e803")) def test_play_tone_and_wait(self, sock, brick): with patch("nxt.brick.time.sleep") as sleep: brick.play_tone_and_wait(440, 1000) assert sock.mock_calls == sent(bytes.fromhex("8003 b801 e803")) assert sleep.mock_calls == [call(1)] def test_set_output_state(self, sock, brick): brick.set_output_state( nxt.motor.Port.B, -100, nxt.motor.Mode.ON, nxt.motor.RegulationMode.IDLE, -5, nxt.motor.RunState.RUNNING, 0x04030201, ) assert sock.mock_calls == sent(bytes.fromhex("8004 01 9c 01 00 fb 20 01020304")) def test_set_input_mode(self, sock, brick): brick.set_input_mode( nxt.sensor.Port.S3, nxt.sensor.Type.SWITCH, nxt.sensor.Mode.BOOL ) assert sock.mock_calls == sent(bytes.fromhex("8005 02 01 20")) def test_get_output_state(self, sock, brick): sock.recv.return_value = bytes.fromhex( "020600 01 9c 01 00 fb 20 01020304 11121314 21222324 31323334" ) ( port, power, mode, regulation_mode, turn_ratio, run_state, tacho_limit, tacho_count, block_tacho_count, rotation_count, ) = brick.get_output_state(nxt.motor.Port.B) assert sock.mock_calls == sent_recved(bytes.fromhex("0006 01")) assert port == nxt.motor.Port.B assert power == -100 assert mode == nxt.motor.Mode.ON assert regulation_mode == nxt.motor.RegulationMode.IDLE assert turn_ratio == -5 assert run_state == nxt.motor.RunState.RUNNING assert tacho_limit == 0x04030201 assert tacho_count == 0x14131211 assert block_tacho_count == 0x24232221 assert rotation_count == 0x34333231 def test_get_input_values(self, sock, brick): sock.recv.return_value = bytes.fromhex( "020700 02 01 00 01 20 0102 1112 2122 3132" ) ( port, valid, calibrated, sensor_type, sensor_mode, raw_value, normalized_value, scaled_value, calibrated_value, ) = brick.get_input_values(nxt.sensor.Port.S3) assert sock.mock_calls == sent_recved(bytes.fromhex("0007 02")) assert port == nxt.sensor.Port.S3 assert valid is True assert calibrated is False assert sensor_type == nxt.sensor.Type.SWITCH assert sensor_mode == nxt.sensor.Mode.BOOL assert raw_value == 0x0201 assert normalized_value == 0x1211 assert scaled_value == 0x2221 assert calibrated_value == 0x3231 def test_reset_input_scaled_value(self, sock, brick): sock.recv.return_value = bytes.fromhex("020800") brick.reset_input_scaled_value(nxt.sensor.Port.S3) assert sock.mock_calls == sent_recved(bytes.fromhex("0008 02")) def test_message_write(self, sock, brick): sock.recv.return_value = bytes.fromhex("020900") brick.message_write(3, bytes.fromhex("21222324252627")) assert sock.mock_calls == sent_recved( bytes.fromhex("0009 03 08 21222324252627 00") ) def test_message_write_too_large(self, sock, brick): with pytest.raises(ValueError): brick.message_write(3, bytes.fromhex("21") * 59) def test_reset_motor_position(self, sock, brick): sock.recv.return_value = bytes.fromhex("020a00") brick.reset_motor_position(nxt.motor.Port.B, True) assert sock.mock_calls == sent_recved(bytes.fromhex("000a 01 01")) def test_get_battery_level(self, sock, brick): sock.recv.return_value = bytes.fromhex("020b00 2823") millivolts = brick.get_battery_level() assert sock.mock_calls == sent_recved(bytes.fromhex("000b")) assert millivolts == 9000 def test_stop_sound_playback(self, sock, brick): sock.recv.return_value = bytes.fromhex("020c00") brick.stop_sound_playback() assert sock.mock_calls == sent_recved(bytes.fromhex("000c")) def test_keep_alive(self, sock, brick): sock.recv.return_value = bytes.fromhex("020d00 01020304") sleep_timeout = brick.keep_alive() assert sock.mock_calls == sent_recved(bytes.fromhex("000d")) assert sleep_timeout == 0x04030201 def test_ls_get_status(self, sock, brick): sock.recv.return_value = bytes.fromhex("020e00 07") size = brick.ls_get_status(nxt.sensor.Port.S3) assert sock.mock_calls == sent_recved(bytes.fromhex("000e 02")) assert size == 7 def test_ls_write(self, sock, brick): sock.recv.return_value = bytes.fromhex("020f00") brick.ls_write(nxt.sensor.Port.S3, bytes.fromhex("21222324252627"), 9) assert sock.mock_calls == sent_recved( bytes.fromhex("000f 02 07 09 21222324252627") ) def test_ls_read(self, sock, brick): sock.recv.return_value = bytes.fromhex( "021000 07 21222324252627 000000000000000000" ) rx_data = brick.ls_read(nxt.sensor.Port.S3) assert sock.mock_calls == sent_recved(bytes.fromhex("0010 02")) assert rx_data == bytes.fromhex("21222324252627") def test_get_current_program_name(self, sock, brick): sock.recv.return_value = bytes.fromhex("021100") + test_rxe_bin name = brick.get_current_program_name() assert sock.mock_calls == sent_recved(bytes.fromhex("0011")) assert name == "test.rxe" def test_message_read(self, sock, brick): sock.recv.return_value = bytes.fromhex("021300 00 07 21222324252627") + bytes( 52 ) local_inbox, message = brick.message_read(10, 0, True) assert sock.mock_calls == sent_recved(bytes.fromhex("0013 0a 00 01")) assert local_inbox == 0 assert message == bytes.fromhex("21222324252627") def test_message_read_fail(self, sock, brick): sock.recv.return_value = bytes.fromhex("021340 00 00") + bytes(59) with pytest.raises(nxt.error.EmptyMailboxError): local_inbox, message = brick.message_read(10, 0, True) class TestFilesModules: """Test nxt.brick files & modules access.""" def test_find_files_none(self, mbrick): mbrick.file_find_first.side_effect = [nxt.error.FileNotFoundError()] results = list(mbrick.find_files("*.*")) assert results == [] assert mbrick.mock_calls == [ call.file_find_first("*.*"), ] def test_find_files_one(self, mbrick): mbrick.file_find_first.return_value = (0x42, "test.rxe", 0x04030201) mbrick.file_find_next.side_effect = [nxt.error.FileNotFoundError()] results = list(mbrick.find_files("*.*")) assert results == [("test.rxe", 0x04030201)] assert mbrick.mock_calls == [ call.file_find_first("*.*"), call.file_find_next(0x42), call.file_close(0x42), ] def test_find_files_two(self, mbrick): mbrick.file_find_first.return_value = (0x42, "test.rxe", 0x04030201) mbrick.file_find_next.side_effect = [ (0x42, "test.rso", 7), nxt.error.FileNotFoundError(), ] results = list(mbrick.find_files("*.*")) assert results == [("test.rxe", 0x04030201), ("test.rso", 7)] assert mbrick.mock_calls == [ call.file_find_first("*.*"), call.file_find_next(0x42), call.file_find_next(0x42), call.file_close(0x42), ] def test_find_files_interrupted(self, mbrick): mbrick.file_find_first.return_value = (0x42, "test.rxe", 0x04030201) mbrick.file_find_next.side_effect = [nxt.error.FileNotFoundError()] g = mbrick.find_files("*.*") assert next(g) == ("test.rxe", 0x04030201) g.close() assert mbrick.mock_calls == [ call.file_find_first("*.*"), call.file_close(0x42), ] def test_find_modules_none(self, mbrick): mbrick.module_find_first.side_effect = [nxt.error.ModuleNotFoundError()] results = list(mbrick.find_modules("*.*")) assert results == [] assert mbrick.mock_calls == [ call.module_find_first("*.*"), ] def test_find_modules_one(self, mbrick): mbrick.module_find_first.return_value = (0x42, "Loader", 0x00090001, 0, 8) mbrick.module_find_next.side_effect = [nxt.error.ModuleNotFoundError()] results = list(mbrick.find_modules("*.*")) assert mbrick.mock_calls == [ call.module_find_first("*.*"), call.module_find_next(0x42), call.module_close(0x42), ] assert results == [("Loader", 0x00090001, 0, 8)] def test_find_modules_two(self, mbrick): mbrick.module_find_first.return_value = (0x42, "Loader", 0x00090001, 0, 8) mbrick.module_find_next.side_effect = [ (0x42, "Dummy", 0x01020304, 0, 12), nxt.error.ModuleNotFoundError(), ] results = list(mbrick.find_modules("*.*")) assert mbrick.mock_calls == [ call.module_find_first("*.*"), call.module_find_next(0x42), call.module_find_next(0x42), call.module_close(0x42), ] assert results == [ ("Loader", 0x00090001, 0, 8), ("Dummy", 0x01020304, 0, 12), ] def test_file_read_text(self, mbrick): mbrick.file_open_read.return_value = (0x42, 12) mbrick.file_read.return_value = (0x42, b"hello\nworld\n") with mbrick.open_file("test.txt") as f: results = list(f) assert results == ["hello\n", "world\n"] assert mbrick.mock_calls == [ call.file_open_read("test.txt"), call.file_read(0x42, 12), call.file_close(0x42), ] def test_file_read_bin(self, mbrick): mbrick.file_open_read.return_value = (0x42, 7) mbrick.file_read.return_value = (0x42, bytes.fromhex("21222324252627")) with mbrick.open_file("test.bin", "rb") as f: assert f.read() == bytes.fromhex("21222324252627") assert mbrick.mock_calls == [ call.file_open_read("test.bin"), call.file_read(0x42, 7), call.file_close(0x42), ] def test_file_read_raw(self, mbrick): mbrick.file_open_read.return_value = (0x42, 7) mbrick.file_read.return_value = (0x42, bytes.fromhex("21222324252627")) with mbrick.open_file("test.bin", "rb", buffering=0) as f: assert f.read() == bytes.fromhex("21222324252627") assert mbrick.mock_calls == [ call.file_open_read("test.bin"), call.file_read(0x42, 7), call.file_close(0x42), ] def test_file_write_text(self, mbrick): mbrick.file_open_write.return_value = 0x42 mbrick.file_write.return_value = (0x42, 12) f = mbrick.open_file("test.txt", "wt", 12) f.write("hello\n") f.write("world\n") f.close() assert mbrick.mock_calls == [ call.file_open_write("test.txt", 12), call.file_write(0x42, b"hello\nworld\n"), call.file_close(0x42), ] def test_file_write_text_encoding(self, mbrick): mbrick.file_open_write.return_value = 0x42 mbrick.file_write.return_value = (0x42, 16) f = mbrick.open_file("test.txt", "wt", 16, errors="replace") f.write("un petit café ?\n") f.close() assert mbrick.mock_calls == [ call.file_open_write("test.txt", 16), call.file_write(0x42, b"un petit caf? ?\n"), call.file_close(0x42), ] def test_file_write_bin(self, mbrick): mbrick.file_open_write.return_value = 0x42 mbrick.file_write.return_value = (0x42, 7) f = mbrick.open_file("test.bin", "wb", 7) f.write(bytes.fromhex("21222324252627")) f.close() assert mbrick.mock_calls == [ call.file_open_write("test.bin", 7), call.file_write(0x42, bytes.fromhex("21222324252627")), call.file_close(0x42), ] def test_file_write_raw(self, mbrick): mbrick.file_open_write.return_value = 0x42 mbrick.file_write.return_value = (0x42, 7) f = mbrick.open_file("test.bin", "wb", 7, buffering=0) f.write(bytes.fromhex("21222324252627")) f.close() assert mbrick.mock_calls == [ call.file_open_write("test.bin", 7), call.file_write(0x42, bytes.fromhex("21222324252627")), call.file_close(0x42), ] def test_file_write_too_much(self, mbrick): mbrick.file_open_write.return_value = 0x42 mbrick.file_write.return_value = (0x42, 7) f = mbrick.open_file("test.bin", "wb", 7) f.write(bytes.fromhex("2122232425262728")) with pytest.raises(ValueError): # Flush done on close. f.close() assert f.closed assert mbrick.mock_calls == [ call.file_open_write("test.bin", 7), call.file_write(0x42, bytes.fromhex("21222324252627")), call.file_close(0x42), ] def test_file_write_closed(self, mbrick): mbrick.file_open_write.return_value = 0x42 mbrick.file_write.return_value = (0x42, 7) # Write directly to raw file to bypass error check in buffered file. f = mbrick.open_file("test.bin", "wb", 7, buffering=0) f.close() assert f.closed with pytest.raises(ValueError): f.write(bytes.fromhex("28")) assert mbrick.mock_calls == [ call.file_open_write("test.bin", 7), call.file_close(0x42), ] def test_file_invalid_params(self, mbrick): with pytest.raises(ValueError): mbrick.open_file("test.bin", "ww", 7) with pytest.raises(ValueError): mbrick.open_file("test.bin", "wr", 7) with pytest.raises(ValueError): mbrick.open_file("test.bin", "tb") with pytest.raises(ValueError): mbrick.open_file("test.bin", "t") with pytest.raises(ValueError): mbrick.open_file("test.bin", "x") with pytest.raises(ValueError): mbrick.open_file("test.bin", "rb", encoding="ascii") with pytest.raises(ValueError): mbrick.open_file("test.bin", "rb", errors="ignore") with pytest.raises(ValueError): mbrick.open_file("test.bin", "rb", newline="\n") with pytest.raises(ValueError): mbrick.open_file("test.bin", "r", buffering=0) with pytest.raises(ValueError): mbrick.open_file("test.bin", "r", 7) with pytest.raises(ValueError): mbrick.open_file("test.bin", "w") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735655274.1831486 nxt_python-3.5.1/tests/test_locator.py0000644000000000000000000001754314734777552015135 0ustar00# test_locator -- Test nxt.locator module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import Mock, call, patch import pytest import nxt.locator def make_backend_mock(): m = Mock(spec_set=("get_backend",)) backend = Mock(spec_set=("find",)) backend.find.return_value = [] m.get_backend.return_value = backend return m @pytest.fixture def mbackend_usb(): return make_backend_mock() @pytest.fixture def mbackend_bluetooth(): return make_backend_mock() @pytest.fixture def mbackend_devfile(): return make_backend_mock() @pytest.fixture def mbackend_socket(): return make_backend_mock() @pytest.fixture(autouse=True) def mimportlib(mbackend_usb, mbackend_bluetooth, mbackend_devfile, mbackend_socket): def import_module(name): if name == "nxt.backend.usb": return mbackend_usb if name == "nxt.backend.bluetooth": return mbackend_bluetooth if name == "nxt.backend.devfile": return mbackend_devfile if name == "nxt.backend.socket": return mbackend_socket raise ImportError("no such module") with patch("nxt.locator.importlib") as m: m.import_module.side_effect = import_module yield m @pytest.fixture(autouse=True) def mconfigparser(): parser = Mock(spec_set=("read", "__contains__", "__getitem__")) parser.read.return_value = ["some", "files"] parser.__contains__ = Mock(return_value=True) parser.__getitem__ = Mock(return_value=dict()) with patch("nxt.locator.configparser") as m: m.ConfigParser.return_value = parser m.DEFAULTSECT = "DEFAULT" yield m def test_find_no_found(): with pytest.raises(nxt.locator.BrickNotFoundError): nxt.locator.find() def test_find_first(mbackend_usb, mbackend_bluetooth, mbrick, mbrick2): mbackend_usb.get_backend().find.return_value = [mbrick] mbackend_bluetooth.get_backend().find.return_value = [mbrick2] assert nxt.locator.find() is mbrick def test_find_first_second_backend(mbackend_usb, mbackend_bluetooth, mbrick): mbackend_usb.get_backend().find.return_value = [] mbackend_bluetooth.get_backend().find.return_value = [mbrick] assert nxt.locator.find() is mbrick def test_find_by_name(mbackend_usb, mbrick, mbrick2): mbackend_usb.get_backend().find.return_value = [mbrick, mbrick2] mbrick.get_device_info.return_value = "NXT", None, None, None mbrick2.get_device_info.return_value = "NXT2", None, None, None assert nxt.locator.find(name="NXT2") is mbrick2 def test_find_by_host(mbackend_usb, mbrick, mbrick2): mbackend_usb.get_backend().find.return_value = [mbrick, mbrick2] mbrick.get_device_info.return_value = "NXT", "00:16:53:00:00:01", None, None mbrick2.get_device_info.return_value = "NXT2", "00:16:53:00:00:02", None, None assert nxt.locator.find(host="00:16:53:00:00:02") is mbrick2 def test_find_by_custom(mbackend_usb, mbrick, mbrick2): mbackend_usb.get_backend().find.return_value = [mbrick, mbrick2] assert nxt.locator.find(custom_match=lambda b: b is mbrick2) is mbrick2 def test_find_all(mbackend_usb, mbackend_bluetooth, mbrick, mbrick2): mbackend_usb.get_backend().find.return_value = [mbrick] mbackend_bluetooth.get_backend().find.return_value = [mbrick2] bricks = list(nxt.locator.find(find_all=True)) assert len(bricks) == 2 assert bricks[0] == mbrick assert bricks[1] == mbrick2 def test_find_filename(mbackend_devfile, mbrick): mbackend_devfile.get_backend().find.return_value = [mbrick] assert nxt.locator.find(filename="/dev/rfcomm0") is mbrick def test_find_socket(mbackend_socket, mbrick): mbackend_socket.get_backend().find.return_value = [mbrick] assert nxt.locator.find(server_host="localhost") is mbrick assert nxt.locator.find(server_port=2727) is mbrick def test_find_bad_backend(): with pytest.raises(ValueError): assert nxt.locator.find(backends=["bad!"]) def test_find_backend_not_found(): with pytest.raises(ImportError): assert nxt.locator.find(backends=["unknown"]) def test_find_no_config(mbackend_usb, mconfigparser, mbrick): mbackend_usb.get_backend().find.return_value = [mbrick] assert nxt.locator.find(config=None) is mbrick assert not mconfigparser.ConfigParser.called def test_find_no_config_section(mbackend_usb, mconfigparser, mbrick): mbackend_usb.get_backend().find.return_value = [mbrick] parser = mconfigparser.ConfigParser.return_value parser.__contains__.return_value = False assert nxt.locator.find() is mbrick assert parser.__contains__.mock_calls == [ call("default"), ] assert parser.__getitem__.mock_calls == [ call("DEFAULT"), ] def test_find_config_backends( mbackend_usb, mbackend_bluetooth, mbackend_devfile, mbackend_socket, mconfigparser, mbrick, ): mbackend_socket.get_backend().find.return_value = [mbrick] parser = mconfigparser.ConfigParser.return_value parser.__getitem__.return_value = dict(backends="devfile socket") assert nxt.locator.find() is mbrick assert not mbackend_usb.get_backend().find.called assert not mbackend_bluetooth.get_backend().find.called assert mbackend_devfile.get_backend().find.called assert mbackend_socket.get_backend().find.called def test_find_config_backends_override( mbackend_usb, mbackend_socket, mconfigparser, mbrick ): mbackend_usb.get_backend().find.return_value = [mbrick] parser = mconfigparser.ConfigParser.return_value parser.__getitem__.return_value = dict(backends="socket") assert nxt.locator.find(backends=["usb"]) is mbrick assert mbackend_usb.get_backend().find.called assert not mbackend_socket.get_backend().find.called def test_find_config(mbackend_usb, mconfigparser, mbrick): mbackend_usb.get_backend().find.return_value = [mbrick] mbrick.get_device_info.return_value = "NXT", "00:16:53:00:00:01", None, None parser = mconfigparser.ConfigParser.return_value parser.__getitem__.return_value = dict(name="NXT", host="00:16:53:00:00:01") assert nxt.locator.find() is mbrick assert mbackend_usb.get_backend().find.mock_calls == [ call(name="NXT", host="00:16:53:00:00:01"), ] def test_find_config_override(mbackend_usb, mconfigparser, mbrick): mbackend_usb.get_backend().find.return_value = [mbrick] mbrick.get_device_info.return_value = "NXT2", "00:16:53:00:00:02", None, None parser = mconfigparser.ConfigParser.return_value parser.__getitem__.return_value = dict(name="NXT", host="00:16:53:00:00:01") assert nxt.locator.find(name="NXT2") is mbrick assert mbackend_usb.get_backend().find.mock_calls == [ call(name="NXT2", host=None), ] def test_find_config_filters(mbackend_usb, mconfigparser, mbrick): mbackend_usb.get_backend().find.return_value = [mbrick] parser = mconfigparser.ConfigParser.return_value parser.__getitem__.return_value = dict(test="testing") assert nxt.locator.find() is mbrick assert mbackend_usb.get_backend().find.mock_calls == [ call(name=None, host=None, test="testing"), ] def test_find_config_filenames(mbackend_usb, mconfigparser, mbrick): mbackend_usb.get_backend().find.return_value = [mbrick] parser = mconfigparser.ConfigParser.return_value assert nxt.locator.find(config_filenames=["some", "files"]) is mbrick assert parser.read.mock_calls == [ call(["some", "files"]), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1639426617.3062723 nxt_python-3.5.1/tests/test_motcont.py0000644000000000000000000001020214155725071015120 0ustar00# test_motcont -- Test nxt.motcont module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import call import pytest import nxt.error import nxt.motcont import nxt.motor @pytest.fixture def mc(mbrick): return nxt.motcont.MotCont(mbrick) def msg(x): """Remove spaces and encode to bytes.""" return x.replace(" ", "").encode("ascii") def test_cmd(mbrick, mtime, mc): mc.cmd(nxt.motor.Port.B, -100, 1000, speedreg=1, smoothstart=0, brake=0) # Using the same port yield a delay. mc.cmd(nxt.motor.Port.B, 10, 0, speedreg=0, smoothstart=1, brake=1) assert mbrick.mock_calls == [ call.message_write(1, msg("1 1 200 001000 2")), call.message_write(1, msg("1 1 010 000000 5")), ] assert mtime.sleep.mock_calls == [ call(0.015), ] def test_cmd_nosleep(mbrick, mtime, mc): mc.cmd(nxt.motor.Port.B, -100, 1000, speedreg=1, smoothstart=0, brake=0) # Using a different port yield no delay. mc.cmd(nxt.motor.Port.C, 10, 0, speedreg=0, smoothstart=1, brake=1) assert mbrick.mock_calls == [ call.message_write(1, msg("1 1 200 001000 2")), call.message_write(1, msg("1 2 010 000000 5")), ] assert mtime.sleep.mock_calls == [] def test_cmd_twomotors(mbrick, mtime, mc): mc.cmd(nxt.motor.Port.B, -100, 1000, speedreg=1, smoothstart=0, brake=0) # When using two motors, there should be a delay as the same motor is used again. mc.cmd( (nxt.motor.Port.B, nxt.motor.Port.C), 10, 0, speedreg=0, smoothstart=1, brake=1 ) assert mbrick.mock_calls == [ call.message_write(1, msg("1 1 200 001000 2")), call.message_write(1, msg("1 5 010 000000 5")), ] assert mtime.sleep.mock_calls == [ call(0.015), ] def test_cmd_threemotors(mbrick, mtime, mc): with pytest.raises(ValueError): mc.cmd( (nxt.motor.Port.A, nxt.motor.Port.B, nxt.motor.Port.C), -100, 1000, speedreg=1, smoothstart=0, brake=0, ) def test_reset_tacho(mbrick, mc): mc.reset_tacho(nxt.motor.Port.B) assert mbrick.mock_calls == [call.message_write(1, msg("2 1"))] def test_is_ready(mbrick, mtime, mc): mbrick.message_read.side_effect = [(1, msg("1 1")), (1, msg("2 0"))] ready = mc.is_ready(nxt.motor.Port.B) not_ready = mc.is_ready(nxt.motor.Port.C) assert mtime.sleep.called assert mbrick.mock_calls == [ call.message_write(1, msg("3 1")), call.message_read(0, 1, 1), call.message_write(1, msg("3 2")), call.message_read(0, 1, 1), ] assert ready is True assert not_ready is False def test_is_ready_error(mbrick, mc): mbrick.message_read.return_value = (1, msg("0 1")) with pytest.raises(nxt.error.ProtocolError): mc.is_ready(nxt.motor.Port.B) def test_set_output_state(mbrick, mc): mc.set_output_state(nxt.motor.Port.C, -100, 1000, speedreg=1) mc.set_output_state(nxt.motor.Port.C, 10, 0, speedreg=0) assert mbrick.mock_calls == [ call.message_write(1, msg("4 2 200 001000 1")), call.message_write(1, msg("4 2 010 000000 0")), ] def test_start(mbrick, mc): mc.start() assert mbrick.mock_calls == [ call.stop_program(), call.start_program("MotorControl22.rxe"), ] def test_start_not_running(mbrick, mc): mbrick.stop_program.side_effect = [nxt.error.DirectProtocolError("")] mc.start() assert mbrick.mock_calls == [ call.stop_program(), call.start_program("MotorControl22.rxe"), ] def test_stop(mbrick): mc = nxt.motcont.MotCont(mbrick) mc.stop() assert mbrick.mock_calls == [ call.stop_program(), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1655935995.5675924 nxt_python-3.5.1/tests/test_motor.py0000644000000000000000000002432714254711774014617 0ustar00# test_motor -- Test nxt.motor module # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. from unittest.mock import MagicMock, call import pytest import nxt.motor from nxt.motor import Mode, Port, RegulationMode, RunState @pytest.fixture def mmotor_factory(mbrick): def factory(port): mbrick.get_output_state.return_value = ( port, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0, 0, 0, 0, ) m = mbrick.get_motor(port) assert mbrick.mock_calls == [call.get_output_state(port)] mbrick.reset_mock() return m return factory @pytest.fixture def mmotor(mmotor_factory): return mmotor_factory(Port.A) @pytest.fixture def mmotorb(mmotor_factory): return mmotor_factory(Port.B) @pytest.fixture def msyncmotor(mmotor, mmotorb): m = nxt.motor.SynchronizedMotors(mmotor, mmotorb, 50) return m def test_reset_position(mbrick, mmotor): mmotor.reset_position(True) mmotor.reset_position(False) assert mbrick.mock_calls == [ call.reset_motor_position(Port.A, True), call.reset_motor_position(Port.A, False), ] def test_run(mbrick, mmotor): mmotor.run() assert mbrick.mock_calls == [ # TODO: should be RegulationMode.IDLE. call.set_output_state( Port.A, 100, Mode.ON, RegulationMode.SPEED, 0, RunState.RUNNING, 0 ), ] def test_run_regulated(mbrick, mmotor): mmotor.run(50, regulated=True) assert mbrick.mock_calls == [ call.set_output_state( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SPEED, 0, RunState.RUNNING, 0, ), ] def test_brake(mbrick, mmotor): mmotor.brake() assert mbrick.mock_calls == [ call.set_output_state( Port.A, 0, Mode.ON | Mode.BRAKE | Mode.REGULATED, RegulationMode.SPEED, 0, RunState.RUNNING, 0, ), ] def test_idle(mbrick, mmotor): mmotor.idle() assert mbrick.mock_calls == [ call.set_output_state( Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), ] def test_weak_turn(mbrick, mmotor): mmotor.weak_turn(50, 360) assert mbrick.mock_calls == [ call.set_output_state( Port.A, 50, Mode.ON, RegulationMode.IDLE, 0, RunState.RUNNING, 360 ), ] def test_turn(mbrick, mmotor, mtime): mbrick.get_output_state.side_effect = [ (Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0, 0, 0, 0), # Test overshoot. ( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SPEED, 0, RunState.RUNNING, 0, 720, 720, 720, ), ] mmotor.turn(50, 360) assert mbrick.mock_calls == [ call.get_output_state(Port.A), call.set_output_state( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SPEED, 0, RunState.RUNNING, 0, ), call.get_output_state(Port.A), call.set_output_state( Port.A, 0, Mode.ON | Mode.REGULATED | Mode.BRAKE, RegulationMode.SPEED, 0, RunState.RUNNING, 0, ), ] def test_turn_blocked(mbrick, mmotor, mtime): mtime.time.side_effect = [0, 0, 1, 2] with pytest.raises(nxt.motor.BlockedException): mmotor.turn(50, 360, brake=False, timeout=0.1, emulate=False) assert mbrick.mock_calls == [ call.get_output_state(Port.A), call.set_output_state( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SPEED, 0, RunState.RUNNING, 360, ), call.get_output_state(Port.A), call.set_output_state( Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), ] def test_turn_stopped(mbrick, mmotor, mtime): # Test if skipping get_tacho() while not "slept enough" # Test motor gets stopped once stop_turn is set to True mbrick.get_output_state.side_effect = [ (Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0, 0, 0, 0), ( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SPEED, 0, RunState.RUNNING, 0, 90, 90, 90, ), ] mtime.time.side_effect = [0, 0, 0.1, 0.2, 0.8, 0.9] stop_motor_mock = MagicMock(side_effect=lambda: False) stop_motor_mock.side_effect = [False, False, False, False, True] mmotor.turn(50, 360, stop_turn=stop_motor_mock) assert mbrick.mock_calls == [ call.get_output_state(Port.A), call.set_output_state( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SPEED, 0, RunState.RUNNING, 0, ), call.get_output_state(Port.A), call.set_output_state( Port.A, 0, Mode.ON | Mode.REGULATED | Mode.BRAKE, RegulationMode.SPEED, 0, RunState.RUNNING, 0, ), ] def test_sync_run(mbrick, msyncmotor): msyncmotor.run(50) assert mbrick.mock_calls == [ call.reset_motor_position(Port.A, True), call.reset_motor_position(Port.B, True), call.set_output_state( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SYNC, # TODO: why reversed? This does not seems right. -50, RunState.RUNNING, 0, ), call.set_output_state( Port.B, 50, Mode.ON | Mode.REGULATED, RegulationMode.SYNC, -50, RunState.RUNNING, 0, ), ] def test_sync_brake(mbrick, msyncmotor): msyncmotor.brake() assert mbrick.mock_calls == [ # TODO: this should be possible to make it simpler. call.set_output_state( Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), call.set_output_state( Port.B, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), call.reset_motor_position(Port.A, True), call.reset_motor_position(Port.B, True), call.set_output_state( Port.A, 0, Mode.ON | Mode.BRAKE | Mode.REGULATED, RegulationMode.SYNC, -50, RunState.RUNNING, 0, ), call.set_output_state( Port.B, 0, Mode.ON | Mode.BRAKE | Mode.REGULATED, RegulationMode.SYNC, -50, RunState.RUNNING, 0, ), call.set_output_state( Port.A, 0, Mode.IDLE, RegulationMode.IDLE, -50, RunState.IDLE, 0 ), call.set_output_state( Port.B, 0, Mode.IDLE, RegulationMode.IDLE, -50, RunState.IDLE, 0 ), call.set_output_state( Port.A, 0, Mode.ON | Mode.BRAKE | Mode.REGULATED, RegulationMode.SPEED, -50, RunState.RUNNING, 0, ), call.set_output_state( Port.B, 0, Mode.ON | Mode.BRAKE | Mode.REGULATED, RegulationMode.SPEED, -50, RunState.RUNNING, 0, ), ] def test_sync_idle(mbrick, msyncmotor): msyncmotor.idle() assert mbrick.mock_calls == [ call.set_output_state( Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), call.set_output_state( Port.B, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), ] def test_sync_turn(mbrick, msyncmotor): mbrick.get_output_state.side_effect = [ (Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0, 0, 0, 0), (Port.B, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0, 0, 0, 0), # Test overshoot. ( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SYNC, 0, RunState.RUNNING, 0, 720, 720, 720, ), ( Port.B, 0, Mode.ON | Mode.REGULATED, RegulationMode.SYNC, 0, RunState.RUNNING, 0, 0, 0, 0, ), ] msyncmotor.turn(50, 360, brake=False) assert mbrick.mock_calls == [ call.reset_motor_position(Port.A, True), call.reset_motor_position(Port.B, True), call.get_output_state(Port.A), call.get_output_state(Port.B), call.set_output_state( Port.A, 50, Mode.ON | Mode.REGULATED, RegulationMode.SYNC, -50, RunState.RUNNING, 0, ), call.set_output_state( Port.B, 50, Mode.ON | Mode.REGULATED, RegulationMode.SYNC, -50, RunState.RUNNING, 0, ), call.get_output_state(Port.A), call.get_output_state(Port.B), call.set_output_state( Port.A, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), call.set_output_state( Port.B, 0, Mode.IDLE, RegulationMode.IDLE, 0, RunState.IDLE, 0 ), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1735691247.2309268 nxt_python-3.5.1/tests/test_sensors.py0000644000000000000000000012154414735105757015154 0ustar00# test_sensors -- Test nxt.sensor modules # Copyright (C) 2021 Nicolas Schodet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import struct from unittest.mock import Mock, call import pytest import nxt.sensor import nxt.sensor.analog import nxt.sensor.digital import nxt.sensor.generic import nxt.sensor.hitechnic import nxt.sensor.mindsensors from nxt.sensor import Mode, Port, Type @pytest.fixture def mdigital(monkeypatch): m = Mock(spec_set=("read_value", "write_value")) monkeypatch.setattr( nxt.sensor.digital.BaseDigitalSensor, "read_value", m.read_value ) monkeypatch.setattr( nxt.sensor.digital.BaseDigitalSensor, "write_value", m.write_value ) return m class TestGeneric: """Test non digital sensors.""" def test_analog(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.analog.BaseAnalogSensor) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4), ] s.set_input_mode(Type.SWITCH, Mode.BOOL) v = s.get_input_values() assert v.port == Port.S1 assert v.valid is True assert v.calibrated is False assert v.sensor_type == Type.SWITCH assert v.sensor_mode == Mode.BOOL assert v.raw_value == 1 assert v.normalized_value == 2 assert v.scaled_value == 3 assert v.calibrated_value == 4 assert str(v).startswith("RawReading(") s.reset_input_scaled_value() assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.SWITCH, Mode.BOOL), call.get_input_values(Port.S1), call.reset_input_scaled_value(Port.S1), ] def test_analog_retry(self, mbrick, mtime): s = mbrick.get_sensor(Port.S1, nxt.sensor.analog.BaseAnalogSensor) mbrick.get_input_values.side_effect = [ (Port.S1, False, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4), (Port.S1, False, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4), (Port.S1, True, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4), ] s.set_input_mode(Type.SWITCH, Mode.BOOL) v = s.get_valid_input_values() assert v.port == Port.S1 assert v.valid is True assert v.calibrated is False assert v.sensor_type == Type.SWITCH assert v.sensor_mode == Mode.BOOL assert v.raw_value == 1 assert v.normalized_value == 2 assert v.scaled_value == 3 assert v.calibrated_value == 4 assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.SWITCH, Mode.BOOL), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.get_input_values(Port.S1), ] assert mtime.sleep.mock_calls == [ call(0.1), call(0.1), ] def test_analog_retry_fail(self, mbrick, mtime): retries = 10 s = mbrick.get_sensor(Port.S1, nxt.sensor.analog.BaseAnalogSensor) mbrick.get_input_values.side_effect = retries * [ (Port.S1, False, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4), ] s.set_input_mode(Type.SWITCH, Mode.BOOL) with pytest.raises(nxt.sensor.analog.InvalidReading): s.get_valid_input_values() assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.SWITCH, Mode.BOOL), ] + retries * [ call.get_input_values(Port.S1), ] assert mtime.sleep.mock_calls == (retries - 1) * [call(0.1)] def test_touch(self, mbrick): assert ( nxt.sensor.generic.Touch.get_sample is nxt.sensor.generic.Touch.is_pressed ) s = mbrick.get_sensor(Port.S1, nxt.sensor.generic.Touch) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.SWITCH, Mode.BOOL, 1023, 1023, 0, 1023), (Port.S1, True, False, Type.SWITCH, Mode.BOOL, 183, 183, 1, 183), ] assert s.is_pressed() is False assert s.is_pressed() is True assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.SWITCH, Mode.BOOL), call.get_input_values(Port.S1), call.get_input_values(Port.S1), ] def test_light(self, mbrick): assert ( nxt.sensor.generic.Light.get_sample is nxt.sensor.generic.Light.get_lightness ) s = mbrick.get_sensor(Port.S1, nxt.sensor.generic.Light) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.LIGHT_ACTIVE, Mode.RAW, 726, 250, 250, 250), (Port.S1, True, False, Type.LIGHT_INACTIVE, Mode.RAW, 823, 107, 107, 107), ] assert s.get_lightness() == 250 s.set_illuminated(False) assert s.get_lightness() == 107 assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LIGHT_ACTIVE, Mode.RAW), call.get_input_values(Port.S1), call.set_input_mode(Port.S1, Type.LIGHT_INACTIVE, Mode.RAW), call.get_input_values(Port.S1), ] def test_sound(self, mbrick): assert ( nxt.sensor.generic.Sound.get_sample is nxt.sensor.generic.Sound.get_loudness ) s = mbrick.get_sensor(Port.S1, nxt.sensor.generic.Sound) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.SOUND_DBA, Mode.RAW, 999, 15, 15, 15), (Port.S1, True, False, Type.SOUND_DB, Mode.RAW, 999, 15, 15, 15), ] assert s.get_loudness() == 15 s.set_adjusted(False) assert s.get_loudness() == 15 assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.SOUND_DBA, Mode.RAW), call.get_input_values(Port.S1), call.set_input_mode(Port.S1, Type.SOUND_DB, Mode.RAW), call.get_input_values(Port.S1), ] def test_color(self, mbrick): assert nxt.sensor.generic.Color.get_sample is nxt.sensor.generic.Color.get_color s = mbrick.get_sensor(Port.S1, nxt.sensor.generic.Color) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.COLOR_FULL, Mode.RAW, 0, 0, 4, 0), (Port.S1, True, False, Type.COLOR_FULL, Mode.RAW, 0, 0, 4, 0), # TODO: handle invalid measures on configuration change. (Port.S1, True, False, Type.COLOR_RED, Mode.RAW, 114, 46, 46, 46), (Port.S1, True, False, Type.COLOR_RED, Mode.RAW, 114, 46, 46, 46), ] color = s.get_color() assert color == 4 assert color == s.DetectedColor.YELLOW assert s.get_reflected_light(Type.COLOR_RED) == 46 assert s.get_light_color() == Type.COLOR_RED assert mbrick.mock_calls == [ # TODO: set too much input mode. call.set_input_mode(Port.S1, Type.COLOR_FULL, Mode.RAW), call.set_input_mode(Port.S1, Type.COLOR_FULL, Mode.RAW), # TODO: get too much input values. call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.set_input_mode(Port.S1, Type.COLOR_RED, Mode.RAW), call.get_input_values(Port.S1), call.get_input_values(Port.S1), ] @pytest.mark.usefixtures("mtime") class TestDigital: """Test nxt.sensor.digital.""" version_bin = b"V1.0\0\0\0\0" product_id_bin = b"LEGO\0\0\0\0" sensor_type_bin = b"Sonar\0\0\0" def test_get_sensor_info(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.digital.BaseDigitalSensor, False) mbrick.ls_get_status.return_value = 8 mbrick.ls_read.side_effect = [ self.version_bin, self.product_id_bin, self.sensor_type_bin, ] info = s.get_sensor_info() assert info.version == "V1.0" assert info.product_id == "LEGO" assert info.sensor_type == "Sonar" print(info) assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW), call.ls_write(Port.S1, bytes((0x02, 0x00)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), call.ls_write(Port.S1, bytes((0x02, 0x08)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), call.ls_write(Port.S1, bytes((0x02, 0x10)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), ] def test_check_compatible(self, mbrick, caplog): class DummySensor(nxt.sensor.digital.BaseDigitalSensor): pass DummySensor.add_compatible_sensor(None, "NXT-PYTH", "Dummy") mbrick.ls_get_status.return_value = 8 mbrick.ls_read.side_effect = [ b"V3\0\0\0\0\0\0", b"NXT-PYTH", b"Dummy\0\0\0", b"V2\0\0\0\0\0\0", b"NXT-PYTH", b"Plop\0\0\0\0", ] DummySensor(mbrick, Port.S1) assert "WARNING" not in caplog.text DummySensor(mbrick, Port.S2) assert "WARNING" in caplog.text def test_write_value(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.digital.BaseDigitalSensor, False) s.I2C_ADDRESS = dict(s.I2C_ADDRESS, command=(0x41, "B")) s.write_value("command", (0x12,)) assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW), call.ls_write(Port.S1, bytes((0x02, 0x41, 0x12)), 0), ] def test_not_ready(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.digital.BaseDigitalSensor, False) mbrick.ls_get_status.side_effect = [nxt.error.I2CPendingError("pending"), 8] mbrick.ls_read.return_value = self.product_id_bin assert s.read_value("product_id") == (self.product_id_bin,) assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW), call.ls_write(Port.S1, bytes((0x02, 0x08)), 8), call.ls_get_status(Port.S1), call.ls_get_status(Port.S1), call.ls_read(Port.S1), ] def test_status_timeout(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.digital.BaseDigitalSensor, False) mbrick.ls_get_status.side_effect = ( [nxt.error.I2CPendingError("pending")] * 30 * 3 ) with pytest.raises(nxt.error.I2CError): s.read_value("product_id") mock_calls = [call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW)] + ( [call.ls_write(Port.S1, bytes((0x02, 0x08)), 8)] + [call.ls_get_status(Port.S1)] * 30 + [call.ls_read(Port.S1)] ) * 3 assert mbrick.mock_calls == mock_calls def test_read_error(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.digital.BaseDigitalSensor, False) mbrick.ls_get_status.side_effect = [8, 8] mbrick.ls_read.side_effect = [self.product_id_bin[1:], self.product_id_bin] assert s.read_value("product_id") == (self.product_id_bin,) assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW), call.ls_write(Port.S1, bytes((0x02, 0x08)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), # Retry. call.ls_write(Port.S1, bytes((0x02, 0x08)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), ] def test_read_timeout(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.digital.BaseDigitalSensor, False) mbrick.ls_get_status.return_value = 8 mbrick.ls_read.return_value = self.product_id_bin[1:] with pytest.raises(nxt.error.I2CError): s.read_value("product_id") mock_calls = [call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW)] + [ call.ls_write(Port.S1, bytes((0x02, 0x08)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), ] * 3 assert mbrick.mock_calls == mock_calls def test_find_class(self): def test(info, cls): found = nxt.sensor.digital.find_class(nxt.sensor.digital.SensorInfo(*info)) assert found is cls test(("V1.0", "LEGO", "Sonar"), nxt.sensor.generic.Ultrasonic) test(("Vx.xx", "mndsnsrs", "CMPS"), nxt.sensor.mindsensors.Compassv2) test(("Vx.xx", "mndsnsrs", "DIST"), nxt.sensor.mindsensors.DIST) test(("V3.20", "mndsnsrs", "ACCL-NX"), nxt.sensor.mindsensors.ACCL) test(("V2.11", "mndsnsrs", "MTRMUX"), nxt.sensor.mindsensors.MTRMUX) test(("V1.16", "mndsnsrs", "LineLdr"), nxt.sensor.mindsensors.LineLeader) test(("V1.20", "mndsnsrs", "NXTServo"), nxt.sensor.mindsensors.Servo) test(("V1.01", "mndsnsrs", "NxTMMX"), nxt.sensor.mindsensors.MMX) test(("V1.02", "mndsnsrs", "NXTHID"), nxt.sensor.mindsensors.HID) test(("V2.00", "mndsnsrs", "PSPNX"), nxt.sensor.mindsensors.PS2) test(("\xfdV1.23", "HiTechnc", "Compass "), nxt.sensor.hitechnic.Compass) test(("\xfdV2.1", "HITECHNC", "Compass "), nxt.sensor.hitechnic.Compass) test( ("\xfdV1.1 ", "HITECHNC", "Accel. "), nxt.sensor.hitechnic.Accelerometer ) test(("Vx.xx", "HITECHNC", "IRRecv "), nxt.sensor.hitechnic.IRReceiver) test(("Vx.xx", "HITECHNC", "NewIRDir"), nxt.sensor.hitechnic.IRSeekerv2) test(("Vx.xx", "HITECHNC", "ColorPD"), nxt.sensor.hitechnic.Colorv2) test(("Vx.xx", "HITECHNC", "ColorPD "), nxt.sensor.hitechnic.Colorv2) test(("Vx.xx", "HiTechnc", "Proto "), nxt.sensor.hitechnic.Prototype) test(("Vx.xx", "HiTechnc", "SuperPro"), nxt.sensor.hitechnic.SuperPro) test(("Vx.xx", "HiTechnc", "ServoCon"), nxt.sensor.hitechnic.ServoCon) test(("Vx.xx", "HiTechnc", "MotorCon"), nxt.sensor.hitechnic.MotorCon) def test_get_sensor(self, mbrick): mbrick.ls_get_status.return_value = 8 mbrick.ls_read.side_effect = [ self.version_bin, self.product_id_bin, self.sensor_type_bin, ] assert mbrick.get_sensor(Port.S1).__class__ is nxt.sensor.generic.Ultrasonic assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW), call.ls_write(Port.S1, bytes((0x02, 0x00)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), call.ls_write(Port.S1, bytes((0x02, 0x08)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), call.ls_write(Port.S1, bytes((0x02, 0x10)), 8), call.ls_get_status(Port.S1), call.ls_read(Port.S1), call.set_input_mode(Port.S1, Type.LOW_SPEED_9V, Mode.RAW), ] class TestGenericDigital: """Test LEGO digital sensors.""" def test_ultrasonic(self, mbrick, mdigital): assert ( nxt.sensor.generic.Ultrasonic.get_sample is nxt.sensor.generic.Ultrasonic.get_distance ) s = mbrick.get_sensor( Port.S1, nxt.sensor.generic.Ultrasonic, check_compatible=False ) mdigital.read_value.side_effect = [ (42,), (b"10E-2m\0",), (1, 2, 3, 4, 5, 6, 7, 8), (4,), (43,), ] assert s.get_distance() == 42 assert s.get_measurement_units() == "10E-2m" assert s.get_all_measurements() == (1, 2, 3, 4, 5, 6, 7, 8) assert s.get_measurement_no(3) == 4 s.command(s.Command.OFF) assert s.get_interval() == 43 s.set_interval(44) assert mdigital.mock_calls == [ call.read_value("measurement_byte_0"), call.read_value("measurement_units"), call.read_value("measurements"), call.read_value("measurement_byte_3"), call.write_value("command", (0,)), call.read_value("continuous_measurement_interval"), call.write_value("continuous_measurement_interval", (44,)), ] def test_temperature(self, mbrick, mdigital): assert ( nxt.sensor.generic.Temperature.get_sample is nxt.sensor.generic.Temperature.get_deg_c ) s = mbrick.get_sensor(Port.S1, nxt.sensor.generic.Temperature) mdigital.read_value.side_effect = [ (1600 * 16,), (1600 * 16,), struct.unpack(">h", b"\xff\x80"), ] assert s.get_deg_c() == 100 assert s.get_deg_f() == 212 assert s.get_deg_c() == -0.5 assert mdigital.mock_calls == [ call.read_value("raw_value"), call.read_value("raw_value"), call.read_value("raw_value"), ] class TestMindsensors: """Test Mindsensors sensors.""" def test_sumoeyes(self, mbrick): s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.SumoEyes) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.LIGHT_ACTIVE, Mode.RAW, 0, 0, 0, 0), (Port.S1, True, False, Type.LIGHT_ACTIVE, Mode.RAW, 0, 350, 0, 0), (Port.S1, True, False, Type.LIGHT_ACTIVE, Mode.RAW, 0, 650, 0, 0), (Port.S1, True, False, Type.LIGHT_ACTIVE, Mode.RAW, 0, 800, 0, 0), ] m = s.get_sample() assert str(m) == "(left: False, right: False)" assert (m.left, m.right) == (False, False) m = s.get_sample() assert (m.left, m.right) == (True, False) m = s.get_sample() assert (m.left, m.right) == (False, True) m = s.get_sample() assert (m.left, m.right) == (True, True) s.set_long_range(True) assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LIGHT_ACTIVE, Mode.RAW), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.set_input_mode(Port.S1, Type.LIGHT_INACTIVE, Mode.RAW), ] def test_compassv2(self, mbrick, mdigital): assert ( nxt.sensor.mindsensors.Compassv2.get_sample is nxt.sensor.mindsensors.Compassv2.get_heading ) s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.Compassv2, False) mdigital.read_value.return_value = (300,) # TODO: should return degrees (divide by 10). assert s.get_heading() == 300 s.command(s.Commands.BEGIN_CALIBRATION) # TODO: get rid of ord, adapt format. assert mdigital.mock_calls == [ call.write_value("command", (ord("I"),)), call.read_value("heading"), call.write_value("command", (ord("C"),)), ] def test_dist(self, mbrick, mdigital): assert ( nxt.sensor.mindsensors.DIST.get_sample is nxt.sensor.mindsensors.DIST.get_distance ) s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.DIST, False) # TODO: get rid of ord, adapt format. mdigital.read_value.side_effect = [(100,), (ord("2"),), (42,), (43,), (44,)] assert s.get_distance() == 100 assert s.get_type() == ord(s.Commands.TYPE_GP2D120) s.command(s.Commands.POWER_OFF) assert s.get_voltage() == 42 assert s.get_min_distance() == 43 assert s.get_max_distance() == 44 assert mdigital.mock_calls == [ call.read_value("distance"), call.read_value("type"), call.write_value("command", (ord("D"),)), call.read_value("voltage"), call.read_value("min_distance"), call.read_value("max_distance"), ] def test_rtc(self, mbrick, mdigital): s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.RTC) # TODO: this one is completely broken: # - Return str instead of int. # - Bad handling of hour format. # - Fields are read one by one (what happen if you read minutes at 11:59 and # hours at 12:00? # - Pretty sure struct_time is not right too. mdigital.read_value.side_effect = [(0x32,)] assert s.get_seconds() == "32" def test_accl(self, mbrick, mdigital): assert ( nxt.sensor.mindsensors.ACCL.get_sample is nxt.sensor.mindsensors.ACCL.get_all_accel ) s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.ACCL, False) # TODO: get rid of ord, adapt format. mdigital.read_value.side_effect = [ (ord("2"),), (42,), (1, 2, 3), (43,), (1, 2, 3), (44,), (45,), ] s.command(s.Commands.SENS_2G) assert s.get_sensitivity() == s.Commands.SENS_2G assert s.get_tilt("x") == 42 assert s.get_all_tilt() == (1, 2, 3) assert s.get_accel("z") == 43 assert s.get_all_accel() == (1, 2, 3) assert s.get_offset("x") == 44 assert s.get_range("x") == 45 s.set_offset("x", 46) s.set_range("x", 47) assert mdigital.mock_calls == [ call.write_value("command", (ord("2"),)), call.read_value("sensitivity"), call.read_value("x_tilt"), call.read_value("all_tilt"), call.read_value("z_accel"), call.read_value("all_accel"), call.read_value("x_offset"), call.read_value("x_range"), call.write_value("x_offset", (46,)), call.write_value("x_range", (47,)), ] def test_mtrmux(self, mbrick, mdigital): assert not hasattr(nxt.sensor.mindsensors.MTRMUX, "get_sample") s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.MTRMUX, False) mdigital.read_value.side_effect = [(1,), (2,)] s.command(s.Commands.FLOAT) s.set_direction(1, 1) s.set_speed(2, 2) assert s.get_direction(3) == 1 assert s.get_speed(4) == 2 assert mdigital.mock_calls == [ call.write_value("command", (0,)), call.write_value("direction_m1", (1,)), call.write_value("speed_m2", (2,)), call.read_value("direction_m3"), call.read_value("speed_m4"), ] def test_lineleader(self, mbrick, mdigital): assert ( nxt.sensor.mindsensors.LineLeader.get_sample is nxt.sensor.mindsensors.LineLeader.get_reading_all ) s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.LineLeader, False) mdigital.read_value.side_effect = [ (-10,), (50,), (0x5A,), (50,), (1, 2, 3, 4, 5, 6, 7, 8), (60,), (11, 12, 13, 14, 15, 16, 17, 18), ] s.command(s.Commands.CALIBRATE_WHITE) assert s.get_steering() == -10 assert s.get_average() == 50 assert s.get_result() == 0x5A s.set_set_point(50) s.set_pid("p", 35) s.set_pid_divisor("i", 10) assert s.get_reading(1) == 50 assert s.get_reading_all() == (1, 2, 3, 4, 5, 6, 7, 8) assert s.get_uncal_reading(2) == 60 assert s.get_uncal_all() == (11, 12, 13, 14, 15, 16, 17, 18) assert mdigital.mock_calls == [ # TODO: get rid of ord, adapt format. call.write_value("command", (ord("W"),)), call.read_value("steering"), call.read_value("average"), call.read_value("result"), call.write_value("set_point", (50,)), call.write_value("kp", (35,)), call.write_value("ki_divisor", (10,)), call.read_value("calibrated_reading_byte1"), call.read_value("all_calibrated_readings"), call.read_value("uncal_sensor2_voltage_byte1"), call.read_value("all_uncal_readings"), ] def test_servo(self, mbrick, mdigital): assert not hasattr(nxt.sensor.mindsensors.Servo, "get_sample") s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.Servo, False) # TODO: command can not work, can not fit two bytes in one byte. mdigital.read_value.side_effect = [(1,), (42,), (43,)] assert s.get_bat_level() == 1 s.set_position(1, 42) assert s.get_position(1) == 42 s.set_speed(2, 43) assert s.get_speed(2) == 43 s.set_quick(3, 44) assert mdigital.mock_calls == [ call.read_value("command"), call.write_value("servo_1_pos", (42,)), call.read_value("servo_1_pos"), call.write_value("servo_2_speed", (43,)), call.read_value("servo_2_speed"), call.write_value("servo_3_quick", (44,)), ] def test_mmx(self, mbrick, mdigital): assert not hasattr(nxt.sensor.mindsensors.MMX, "get_sample") s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.MMX, False) mdigital.read_value.side_effect = [ (1,), (0xAA,), (0x55,), (12345,), (0x55,), (42,), ] # TODO: get rid of ord, adapt format. s.command(s.Commands.RESET_PARAMS_ENCODERS) assert s.get_bat_level() == 1 s.set_encoder_target(1, 12345) s.set_speed(2, 50) s.set_time_run(1, 60) s.command_b(1, 42) s.command_a(2, 2, 1) s.command_a(2, 2, 0) assert s.get_encoder_pos(1) == 12345 # TODO: should be bool. assert s.get_motor_status(1, 2) == 1 assert s.get_tasks(1) == 42 s.set_pid("p", "speed", 3) s.set_pass_count(43) s.set_tolerance(44) assert mdigital.mock_calls == [ call.write_value("command", (ord("R"),)), call.read_value("command"), call.write_value("encoder_1_target", (12345,)), call.write_value("speed_2", (50,)), call.write_value("seconds_to_run_1", (60,)), call.write_value("command_b_1", (42,)), call.read_value("command_a_2"), call.write_value("command_a_2", (0xAE,)), call.read_value("command_a_2"), call.write_value("command_a_2", (0x51,)), call.read_value("encoder_1_pos"), call.read_value("status_m1"), call.read_value("tasks_running_m1"), call.write_value("p_speed", (3,)), call.write_value("pass_count", (43,)), call.write_value("tolerance", (44,)), ] def test_hid(self, mbrick, mdigital): assert not hasattr(nxt.sensor.mindsensors.HID, "get_sample") s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.HID, False) s.command(s.Commands.ASCII_MODE) s.set_modifier(42) s.write_data("a") assert mdigital.mock_calls == [ call.write_value("command", (ord("A"),)), call.write_value("modifier", (42,)), call.write_value("keyboard_data", (ord("a"),)), ] def test_ps2(self, mbrick, mdigital): s = mbrick.get_sensor(Port.S1, nxt.sensor.mindsensors.PS2, False) mdigital.read_value.side_effect = [ (42,), (0x55,), # TODO: read everything in one read. (0x55,), (0x55,), (42,), (43,), (44,), (45,), ] s.command(s.Commands.POWER_ON) assert s.get_joystick("x", "left") == 42 assert s.get_buttons(1) == 0x55 st = s.get_sample() assert st.leftstick == (42, 43) assert st.rightstick == (44, 45) assert st.buttons.left is True assert st.buttons.down is False assert st.buttons.right is True assert st.buttons.up is False assert st.buttons.square is True assert st.buttons.cross is False assert st.buttons.circle is True assert st.buttons.triangle is False assert st.buttons.r1 is True assert st.buttons.r2 is True assert st.buttons.r3 is False assert st.buttons.l1 is False assert st.buttons.l2 is False assert st.buttons.l3 is True assert mdigital.mock_calls == [ call.write_value("command", (ord("E"),)), call.read_value("x_left_joystick"), call.read_value("button_set_1"), call.read_value("button_set_1"), call.read_value("button_set_2"), call.read_value("x_left_joystick"), call.read_value("y_left_joystick"), call.read_value("x_right_joystick"), call.read_value("y_right_joystick"), ] class TestHitechnic: """Test HiTechnic sensors.""" def test_compass(self, mbrick, mdigital): assert ( nxt.sensor.hitechnic.Compass.get_sample is nxt.sensor.hitechnic.Compass.get_heading ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.Compass, False) mdigital.read_value.return_value = (10,) assert s.get_heading() == 30 assert s.get_relative_heading(0) == 30 assert s.get_relative_heading(-170) == -160 mdigital.read_value.return_value = (-10,) assert s.get_relative_heading(170) == 160 assert s.is_in_range(-40, -20) is True assert s.is_in_range(-20, -40) is False mdigital.read_value.return_value = (0x02,) assert s.get_mode() == s.Modes.CALIBRATION_FAILED s.set_mode(s.Modes.CALIBRATION) with pytest.raises(ValueError): s.set_mode(s.Modes.CALIBRATION_FAILED) assert mdigital.mock_calls == [ call.read_value("heading"), call.read_value("adder"), ] * 6 + [ call.read_value("mode"), call.write_value("mode", (0x43,)), ] def test_accelerometer(self, mbrick, mdigital): assert ( nxt.sensor.hitechnic.Accelerometer.get_sample is nxt.sensor.hitechnic.Accelerometer.get_acceleration ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.Accelerometer, False) mdigital.read_value.return_value = (0x12, 0x23, -0x32, 0x3, 0x0, 0x2) v = s.get_acceleration() assert (v.x, v.y, v.z) == (75, 140, -198) assert mdigital.mock_calls == [ call.read_value("all_data"), ] def test_irreceiver(self, mbrick, mdigital): assert ( nxt.sensor.hitechnic.IRReceiver.get_sample is nxt.sensor.hitechnic.IRReceiver.get_speeds ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.IRReceiver, False) mdigital.read_value.return_value = (0, -16, 30, -44, 58, -72, 100, -128) v = s.get_speeds() assert (v.m1A, v.m1B) == (0, -16) assert v.channel_1 == (0, -16) assert (v.m2A, v.m2B) == (30, -44) assert v.channel_2 == (30, -44) assert (v.m3A, v.m3B) == (58, -72) assert v.channel_3 == (58, -72) # TODO: handle -128 specially. assert (v.m4A, v.m4B) == (100, -128) assert v.channel_4 == (100, -128) assert mdigital.mock_calls == [ call.read_value("all_data"), ] def test_irseekerv2(self, mbrick, mdigital): assert ( nxt.sensor.hitechnic.IRSeekerv2.get_sample is nxt.sensor.hitechnic.IRSeekerv2.get_ac_values ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.IRSeekerv2, False) mdigital.read_value.side_effect = [ (5, 42, 43, 44, 45, 46, 44), (5, 42, 43, 44, 45, 46), (0,), ] v = s.get_dc_values() assert v.direction == 5 # TODO: use a tuple. assert v.sensor_1 == 42 assert v.sensor_2 == 43 assert v.sensor_3 == 44 assert v.sensor_4 == 45 assert v.sensor_5 == 46 assert v.sensor_mean == 44 assert v.get_dir_brightness(5) == 44 assert v.get_dir_brightness(4) == 43.5 v = s.get_ac_values() assert v.direction == 5 assert v.sensor_1 == 42 assert v.sensor_2 == 43 assert v.sensor_3 == 44 assert v.sensor_4 == 45 assert v.sensor_5 == 46 assert v.get_dir_brightness(5) == 44 assert v.get_dir_brightness(4) == 43.5 assert s.get_dsp_mode() == s.DSPModes.AC_DSP_1200Hz s.set_dsp_mode(s.DSPModes.AC_DSP_600Hz) assert mdigital.mock_calls == [ call.read_value("all_DC"), call.read_value("all_AC"), call.read_value("dspmode"), call.write_value("dspmode", (1,)), ] def test_eopd(self, mbrick): assert ( nxt.sensor.hitechnic.EOPD.get_sample is nxt.sensor.hitechnic.EOPD.get_scaled_value ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.EOPD) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.LIGHT_INACTIVE, Mode.RAW, 523, 0, 0, 0), (Port.S1, True, False, Type.LIGHT_INACTIVE, Mode.RAW, 398, 0, 0, 0), (Port.S1, True, False, Type.LIGHT_INACTIVE, Mode.RAW, 398, 0, 0, 0), (Port.S1, True, False, Type.LIGHT_INACTIVE, Mode.RAW, 1023, 0, 0, 0), ] # TODO: choose a mode in constructor, or raise error if no mode chosen. s.set_range_long() s.set_range_short() assert s.get_raw_value() == 500 assert s.get_processed_value() == 25 assert s.get_scaled_value() == 10 assert s.get_scaled_value() == 250 assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.LIGHT_ACTIVE, Mode.RAW), call.set_input_mode(Port.S1, Type.LIGHT_INACTIVE, Mode.RAW), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.get_input_values(Port.S1), ] def test_colorv2(self, mbrick, mdigital): assert ( nxt.sensor.hitechnic.Colorv2.get_sample is nxt.sensor.hitechnic.Colorv2.get_active_color ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.Colorv2, False) mdigital.read_value.side_effect = [ (8, 100, 50, 0, 75, 42, 66, 33, 0), (100, 50, 0, 75), (0,), ] v = s.get_active_color() assert v.number == 8 assert v.red == 100 assert v.green == 50 assert v.blue == 0 assert v.white == 75 assert v.index == 42 assert v.normred == 66 assert v.normgreen == 33 assert v.normblue == 0 v = s.get_passive_color() assert v.red == 100 assert v.green == 50 assert v.blue == 0 assert v.white == 75 assert s.get_mode() == s.Modes.ACTIVE s.set_mode(s.Modes.PASSIVE) assert mdigital.mock_calls == [ call.read_value("all_data"), call.read_value("all_raw_data"), call.read_value("mode"), call.write_value("mode", (1,)), ] def test_giro(self, mbrick): assert ( nxt.sensor.hitechnic.Gyro.get_sample is nxt.sensor.hitechnic.Gyro.get_rotation_speed ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.Gyro) mbrick.get_input_values.side_effect = [ (Port.S1, True, False, Type.ANGLE, Mode.RAW, 0, 0, 42, 0), (Port.S1, True, False, Type.ANGLE, Mode.RAW, 0, 0, 42, 0), (Port.S1, True, False, Type.ANGLE, Mode.RAW, 0, 0, 54, 0), (Port.S1, True, False, Type.ANGLE, Mode.RAW, 0, 0, 54, 0), ] assert s.get_rotation_speed() == 42 s.calibrate() assert s.get_rotation_speed() == 12 s.set_zero(0) assert s.get_rotation_speed() == 54 assert mbrick.mock_calls == [ call.set_input_mode(Port.S1, Type.ANGLE, Mode.RAW), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.get_input_values(Port.S1), call.get_input_values(Port.S1), ] def test_prototype(self, mbrick, mdigital): assert not hasattr(nxt.sensor.hitechnic.Prototype, "get_sample") s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.Prototype, False) mdigital.read_value.side_effect = [ (42, 43, 44, 45, 46), (0x2A,), ] v = s.get_analog() assert v.a0 == 42 assert v.a1 == 43 assert v.a2 == 44 assert v.a3 == 45 assert v.a4 == 46 v = s.get_digital() assert v.dataint == 0x2A assert v.datalst == [False, True, False, True, False, True] assert list(v) == [False, True, False, True, False, True] assert v[0] is False assert v.d0 is False assert v.d1 is True assert v.d2 is False assert v.d3 is True assert v.d4 is False assert v.d5 is True s.set_digital(s.Digital_Data(0x15)) s.set_digital_modes(s.Digital_Data((False, True, False, True, False, True))) assert mdigital.mock_calls == [ call.read_value("all_analog"), call.read_value("digital_in"), call.write_value("digital_out", (0x15,)), call.write_value("digital_cont", (0x2A,)), ] def test_superpro(self, mbrick, mdigital): s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.SuperPro, False) mdigital.read_value.side_effect = [[0x00], [0x00], [0x00], [0x00]] s.get_analog() mdigital.read_value.side_effect = [[0x00], [0x00], [0x00], [0x00]] s.get_analog_volts() mdigital.read_value.side_effect = [[0x00]] s.get_digital() s.set_digital([0, 1, 0, 1, 0, 1, 0, 1]) s.set_digital_byte(0x00) s.set_digital_modes([0, 1, 0, 1, 0, 1, 0, 1]) s.set_digital_modes_byte(0x00) s.set_strobe_output(0x00) s.set_led_output(0x00) s.analog_out(0, nxt.sensor.hitechnic.SuperPro.AnalogOutputMode.SQUARE, 1, 1023) s.analog_out_voltage( 0, nxt.sensor.hitechnic.SuperPro.AnalogOutputMode.SQUARE, 1, 3.3 ) def test_servocon(self, mbrick, mdigital): assert not hasattr(nxt.sensor.hitechnic.ServoCon, "get_sample") s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.ServoCon, False) mdigital.read_value.side_effect = [ (1,), (43,), ] assert s.get_status() == s.Status.RUNNING s.set_step_time(5) s.set_pos(1, 42) assert s.get_pwm() == 43 s.set_pwm(44) assert mdigital.mock_calls == [ call.read_value("status"), call.write_value("steptime", (5,)), call.write_value("s1pos", (42,)), call.read_value("pwm"), call.write_value("pwm", (44,)), ] def test_motorcon(self, mbrick, mdigital): assert not hasattr(nxt.sensor.hitechnic.MotorCon, "get_sample") s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.MotorCon, False) mdigital.read_value.side_effect = [ (123456,), (654321,), (0x55,), (43,), (1,), (3, 2, 1), (0x70, 0x2), ] s.set_enc_target(1, 123456) assert s.get_enc_target(1) == 123456 assert s.get_enc_current(2) == 654321 s.set_mode(1, 0x55) assert s.get_mode(2) == 0x55 s.set_power(1, 42) assert s.get_power(2) == 43 s.set_gear_ratio(1, 2) assert s.get_gear_ratio(2) == 1 s.set_pid(1, s.PID_Data(3, 2, 1)) v = s.get_pid(2) assert (v.p, v.i, v.d) == (3, 2, 1) assert s.get_battery_voltage() == 450 assert mdigital.mock_calls == [ call.write_value("m1enctarget", (123456,)), call.read_value("m1enctarget"), call.read_value("m2enccurrent"), call.write_value("m1mode", (0x55,)), call.read_value("m2mode"), call.write_value("m1power", (42,)), call.read_value("m2power"), call.write_value("m1gearratio", (2,)), call.read_value("m2gearratio"), call.write_value("m1pid", (3, 2, 1)), call.read_value("m2pid"), call.read_value("batteryvoltage"), ] def test_angle(self, mbrick, mdigital): assert ( nxt.sensor.hitechnic.Angle.get_sample is nxt.sensor.hitechnic.Angle.get_angle ) s = mbrick.get_sensor(Port.S1, nxt.sensor.hitechnic.Angle, False) mdigital.read_value.side_effect = [ (21, 1), (123456789,), (16,), ] assert s.get_angle() == 43 assert s.get_accumulated_angle() == 123456789 assert s.get_rpm() == 16 s.calibrate() s.reset() assert mdigital.mock_calls == [ call.read_value("angle"), call.read_value("angle_acc"), call.read_value("rpm"), call.write_value("mode", b"C"), call.write_value("mode", b"R"), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736613831.1611822 nxt_python-3.5.1/tox.ini0000644000000000000000000000057714740517707012220 0ustar00# tox (https://tox.readthedocs.io/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py39, py310, py311, py312 isolated_build = True [testenv] deps = pytest pyusb pillow commands = pytest nxt_python-3.5.1/setup.py0000644000000000000000000001105600000000000012336 0ustar00# -*- coding: utf-8 -*- from setuptools import setup packages = \ ['nxt', 'nxt.backend', 'nxt.command', 'nxt.sensor'] package_data = \ {'': ['*']} install_requires = \ ['pyusb>=1.2.1,<2.0.0'] extras_require = \ {'bluetooth': ['pybluez>=0.23,<0.24'], 'screenshot': ['pillow>=9.4.0,<10.0.0']} entry_points = \ {'console_scripts': ['nxt-push = nxt.command.push:run', 'nxt-screenshot = nxt.command.screenshot:run', 'nxt-server = nxt.command.server:run', 'nxt-test = nxt.command.test:run']} setup_kwargs = { 'name': 'nxt-python', 'version': '3.5.1', 'description': 'LEGO Mindstorms NXT Control Package', 'long_description': '# ![NXT-Python](./logo.svg)\n\nNXT-Python is a package for controlling a LEGO NXT robot using the Python\nprogramming language. It can communicate using either USB or Bluetooth.\n\nNXT-Python for Python 2 is no longer supported.\n\nNXT-Python repository is on [sourcehut][] with a mirror on [Github][].\n\n[sourcehut]: https://sr.ht/~ni/nxt-python/ "NXT-Python repository on sourcehut"\n[Github]: https://github.com/schodet/nxt-python "NXT-Python repository on Github"\n\n## Requirements\n\n- [Python 3.x](https://www.python.org)\n- USB communication:\n - [PyUSB](https://github.com/pyusb/pyusb)\n- Bluetooth communication:\n - [PyBluez](https://github.com/pybluez/pybluez)\n\n## Installation\n\nInstall NXT-Python with pip:\n\n python3 -m pip install --upgrade nxt-python\n\nSee [installation][] instructions in the documentation for more informations.\n\n[installation]: https://ni.srht.site/nxt-python/latest/installation.html\n\n## Next steps\n\nYou can read the [documentation][], or start directly with the [tutorial][].\n\n[documentation]: https://ni.srht.site/nxt-python/latest/\n[tutorial]: https://ni.srht.site/nxt-python/latest/handbook/tutorial.html\n\n## Upgrading your code\n\nIf you used previous version of NXT-Python with Python 2, the documentation\nincludes an [migration guide][].\n\n[migration guide]: https://ni.srht.site/nxt-python/latest/migration.html\n\n## Contact\n\nThere is a [mailing list][] for questions.\n\nNXT-Python repository maintainer is Nicolas Schodet, since 2021-11-06. You can\ncontact him on the mailing list.\n\nYou can use the [Github issues page][] to report problems, but please use the\nmailing list for questions.\n\n[mailing list]: https://lists.sr.ht/~ni/nxt-python\n[Github issues page]: https://github.com/schodet/nxt-python/issues\n\n## Thanks\n\n- Doug Lau for writing NXT\\_Python, our starting point.\n- rhn for creating what would become v2, making lots of smaller changes, and\n reviewing tons of code.\n- Marcus Wanner for maintaining NXT-Python up to v2.2.2, his work has been\n amazing!\n- Elvin Luff for taking over the project after Marcus, making a lot of work\n for the port to Python 3.\n- mindsensors.com (esp. Ryan Kneip) for helping out with the code for a lot of\n their sensors, expanding the sensors covered by the type checking database,\n and providing hardware for testing.\n- HiTechnic for providing identification information for their sensors. I note\n that they have now included this information in their website. ;)\n- Linus Atorf, Samuel Leeman-Munk, melducky, Simon Levy, Steve Castellotti,\n Paulo Vieira, zonedabone, migpics, TC Wan, jerradgenson, henryacev, Paul\n Hollensen, and anyone else I forgot for various fixes and additions.\n- Goldsloth for making some useful changes and keeping the tickets moving\n after the migration to Github.\n- All our users for their interest and support!\n\n## License\n\nNXT-Python is free software: you can redistribute it and/or modify it under\nthe terms of the GNU General Public License as published by the Free Software\nFoundation, either version 3 of the License, or (at your option) any later\nversion.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT\nANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\nFOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with\nthis program. If not, see .\n', 'author': 'Nicolas Schodet', 'author_email': 'nico@ni.fr.eu.org', 'maintainer': 'None', 'maintainer_email': 'None', 'url': 'https://sr.ht/~ni/nxt-python/', 'packages': packages, 'package_data': package_data, 'install_requires': install_requires, 'extras_require': extras_require, 'entry_points': entry_points, 'python_requires': '>=3.9,<4.0', } setup(**setup_kwargs) nxt_python-3.5.1/PKG-INFO0000644000000000000000000001041200000000000011714 0ustar00Metadata-Version: 2.1 Name: nxt-python Version: 3.5.1 Summary: LEGO Mindstorms NXT Control Package Home-page: https://sr.ht/~ni/nxt-python/ License: GPL-3.0-or-later Author: Nicolas Schodet Author-email: nico@ni.fr.eu.org Requires-Python: >=3.9,<4.0 Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Provides-Extra: bluetooth Provides-Extra: screenshot Requires-Dist: pillow (>=9.4.0,<10.0.0) ; extra == "screenshot" Requires-Dist: pybluez (>=0.23,<0.24) ; extra == "bluetooth" Requires-Dist: pyusb (>=1.2.1,<2.0.0) Project-URL: Documentation, https://ni.srht.site/nxt-python/latest/ Project-URL: Repository, https://git.sr.ht/~ni/nxt-python Description-Content-Type: text/markdown # ![NXT-Python](./logo.svg) NXT-Python is a package for controlling a LEGO NXT robot using the Python programming language. It can communicate using either USB or Bluetooth. NXT-Python for Python 2 is no longer supported. NXT-Python repository is on [sourcehut][] with a mirror on [Github][]. [sourcehut]: https://sr.ht/~ni/nxt-python/ "NXT-Python repository on sourcehut" [Github]: https://github.com/schodet/nxt-python "NXT-Python repository on Github" ## Requirements - [Python 3.x](https://www.python.org) - USB communication: - [PyUSB](https://github.com/pyusb/pyusb) - Bluetooth communication: - [PyBluez](https://github.com/pybluez/pybluez) ## Installation Install NXT-Python with pip: python3 -m pip install --upgrade nxt-python See [installation][] instructions in the documentation for more informations. [installation]: https://ni.srht.site/nxt-python/latest/installation.html ## Next steps You can read the [documentation][], or start directly with the [tutorial][]. [documentation]: https://ni.srht.site/nxt-python/latest/ [tutorial]: https://ni.srht.site/nxt-python/latest/handbook/tutorial.html ## Upgrading your code If you used previous version of NXT-Python with Python 2, the documentation includes an [migration guide][]. [migration guide]: https://ni.srht.site/nxt-python/latest/migration.html ## Contact There is a [mailing list][] for questions. NXT-Python repository maintainer is Nicolas Schodet, since 2021-11-06. You can contact him on the mailing list. You can use the [Github issues page][] to report problems, but please use the mailing list for questions. [mailing list]: https://lists.sr.ht/~ni/nxt-python [Github issues page]: https://github.com/schodet/nxt-python/issues ## Thanks - Doug Lau for writing NXT\_Python, our starting point. - rhn for creating what would become v2, making lots of smaller changes, and reviewing tons of code. - Marcus Wanner for maintaining NXT-Python up to v2.2.2, his work has been amazing! - Elvin Luff for taking over the project after Marcus, making a lot of work for the port to Python 3. - mindsensors.com (esp. Ryan Kneip) for helping out with the code for a lot of their sensors, expanding the sensors covered by the type checking database, and providing hardware for testing. - HiTechnic for providing identification information for their sensors. I note that they have now included this information in their website. ;) - Linus Atorf, Samuel Leeman-Munk, melducky, Simon Levy, Steve Castellotti, Paulo Vieira, zonedabone, migpics, TC Wan, jerradgenson, henryacev, Paul Hollensen, and anyone else I forgot for various fixes and additions. - Goldsloth for making some useful changes and keeping the tickets moving after the migration to Github. - All our users for their interest and support! ## License NXT-Python is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see .