diff --git a/pySX127x/LICENSE b/pySX127x/LICENSE new file mode 100755 index 0000000..2def0e8 --- /dev/null +++ b/pySX127x/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/pySX127x/README.md b/pySX127x/README.md new file mode 100755 index 0000000..6bf4df4 --- /dev/null +++ b/pySX127x/README.md @@ -0,0 +1,319 @@ +# Overview + +This is a python interface to the [Semtech SX1276/7/8/9](http://www.semtech.com/wireless-rf/rf-transceivers/) +long range, low power transceiver family. + +The SX127x have both LoRa and FSK capabilities. Here the focus lies on the +LoRa spread spectrum modulation hence only the LoRa modem interface is implemented so far +(but see the [roadmap](#roadmap) below for future plans). + +Spread spectrum modulation has a number of intriguing features: +* High interference immunity +* Up to 20dBm link budget advantage (for the SX1276/7/8/9) +* High Doppler shift immunity + +More information about LoRa can be found on the [LoRa Alliance website](https://lora-alliance.org). +Links to some LoRa performance reports can be found in the [references](#references) section below. + + +# Motivation + +Transceiver modules are usually interfaced with microcontroller boards such as the +Arduino and there are already many fine C/C++ libraries for the SX127x family available on +[github](https://github.com/search?q=sx127x) and [mbed.org](https://developer.mbed.org/search/?q=sx127x). + +Although C/C++ is the de facto standard for development on microcontrollers, [python](https://www.python.org) +running on a Raspberry Pi (NanoPi, BananaPi, UDOO Neo, BeagleBoard, etc. etc.) is becoming a viable alternative for rapid prototyping. + +High level programming languages like python require a full-blown OS such as Linux. (There are some exceptions like +[MicroPython](https://micropython.org) and its fork [CircuitPython](https://www.adafruit.com/circuitpython).) +But using hardware capable of running Linux contradicts, to some extent, the low power specification of the SX127x family. +Therefore it is clear that this approach aims mostly at prototyping and technology testing. + +Prototyping on a full-blown OS using high level programming languages has several clear advantages: +* Working prototypes can be built quickly +* Technology testing ist faster +* Proof of concept is easier to achieve +* The application development phase is reached quicker + + +# Hardware + +The transceiver module is a SX1276 based Modtronix [inAir9B](http://modtronix.com/inair9.html). +It is mounted on a prototyping board to a Raspberry Pi rev 2 model B. + +| Proto board pin | RaspPi GPIO | Direction | +|:----------------|:-----------:|:---------:| +| inAir9B DIO0 | GPIO 22 | IN | +| inAir9B DIO1 | GPIO 23 | IN | +| inAir9B DIO2 | GPIO 24 | IN | +| inAir9B DIO3 | GPIO 25 | IN | +| inAir9b Reset | GPIO ? | OUT | +| LED | GPIO 18 | OUT | +| Switch | GPIO 4 | IN | + +Todo: +- [ ] Add picture(s) +- [ ] Wire the SX127x reset to a GPIO? + + +# Code Examples + +### Overview +First import the modules +```python +from SX127x.LoRa import * +from SX127x.board_config import BOARD +``` +then set up the board GPIOs +```python +BOARD.setup() +``` +The LoRa object is instantiated and put into the standby mode +```python +lora = LoRa() +lora.set_mode(MODE.STDBY) +``` +Registers are queried like so: +```python +print lora.version() # this prints the sx127x chip version +print lora.get_freq() # this prints the frequency setting +``` +and setting registers is easy, too +```python +lora.set_freq(433.0) # Set the frequency to 433 MHz +``` +In applications the `LoRa` class should be subclassed while overriding one or more of the callback functions that +are invoked on successful RX or TX operations, for example. +```python +class MyLoRa(LoRa): + + def __init__(self, verbose=False): + super(MyLoRa, self).__init__(verbose) + # setup registers etc. + + def on_rx_done(self): + payload = self.read_payload(nocheck=True) + # etc. +``` + +In the end the resources should be freed properly +```python +BOARD.teardown() +``` + +### More details +Most functions of `SX127x.Lora` are setter and getter functions. For example, the setter and getter for +the coding rate are demonstrated here +```python +print lora.get_coding_rate() # print the current coding rate +lora.set_coding_rate(CODING_RATE.CR4_6) # set it to CR4_6 +``` + +@todo + + +# Installation + +Make sure SPI is activated on you RaspberryPi: [SPI](https://www.raspberrypi.org/documentation/hardware/raspberrypi/spi/README.md) +**pySX127x** requires these two python packages: +* [RPi.GPIO](https://pypi.python.org/pypi/RPi.GPIO") for accessing the GPIOs, it should be already installed on + a standard Raspian Linux image +* [spidev](https://pypi.python.org/pypi/spidev) for controlling SPI + +In order to install spidev download the source code and run setup.py manually: +```bash +wget https://pypi.python.org/packages/source/s/spidev/spidev-3.1.tar.gz +tar xfvz spidev-3.1.tar.gz +cd spidev-3.1 +sudo python setup.py install +``` + +At this point you may want to confirm that the unit tests pass. See the section [Tests](#tests) below. + +You can now run the scripts. For example dump the registers with `lora_util.py`: +```bash +rasp$ sudo ./lora_util.py +SX127x LoRa registers: + mode SLEEP + freq 434.000000 MHz + coding_rate CR4_5 + bw BW125 + spreading_factor 128 chips/symb + implicit_hdr_mode OFF + ... and so on .... +``` + + +# Class Reference + +The interface to the SX127x LoRa modem is implemented in the class `SX127x.LoRa.LoRa`. +The most important modem configuration parameters are: + +| Function | Description | +|------------------|---------------------------------------------| +| set_mode | Change OpMode, use the constants.MODE class | +| set_freq | Set the frequency | +| set_bw | Set the bandwidth 7.8kHz ... 500kHz | +| set_coding_rate | Set the coding rate 4/5, 4/6, 4/7, 4/8 | +| | | +| @todo | | + +Most set_* functions have a mirror get_* function, but beware that the getter return types do not necessarily match +the setter input types. + +### Register naming convention +The register addresses are defined in class `SX127x.constants.REG` and we use a specific naming convention which +is best illustrated by a few examples: + +| Register | Modem | Semtech doc. | pySX127x | +|----------|-------|-------------------| ---------------------------| +| 0x0E | LoRa | RegFifoTxBaseAddr | REG.LORA.FIFO_TX_BASE_ADDR | +| 0x0E | FSK | RegRssiCOnfig | REG.FSK.RSSI_CONFIG | +| 0x1D | LoRa | RegModemConfig1 | REG.LORA.MODEM_CONFIG_1 | +| etc. | | | | + +### Hardware +Hardware related definition and initialisation are located in `SX127x.board_config.BOARD`. +If you use a SBC other than the Raspberry Pi you'll have to adapt the BOARD class. + + +# Script references + +### Continuous receiver `rx_cont.py` +The SX127x is put in RXCONT mode and continuously waits for transmissions. Upon a successful read the +payload and the irq flags are printed to screen. +``` +usage: rx_cont.py [-h] [--ocp OCP] [--sf SF] [--freq FREQ] [--bw BW] + [--cr CODING_RATE] [--preamble PREAMBLE] + +Continous LoRa receiver + +optional arguments: + -h, --help show this help message and exit + --ocp OCP, -c OCP Over current protection in mA (45 .. 240 mA) + --sf SF, -s SF Spreading factor (6...12). Default is 7. + --freq FREQ, -f FREQ Frequency + --bw BW, -b BW Bandwidth (one of BW7_8 BW10_4 BW15_6 BW20_8 BW31_25 + BW41_7 BW62_5 BW125 BW250 BW500). Default is BW125. + --cr CODING_RATE, -r CODING_RATE + Coding rate (one of CR4_5 CR4_6 CR4_7 CR4_8). Default + is CR4_5. + --preamble PREAMBLE, -p PREAMBLE + Preamble length. Default is 8. +``` + +### Simple LoRa beacon `tx_beacon.py` +A small payload is transmitted in regular intervals. +``` +usage: tx_beacon.py [-h] [--ocp OCP] [--sf SF] [--freq FREQ] [--bw BW] + [--cr CODING_RATE] [--preamble PREAMBLE] [--single] + [--wait WAIT] + +A simple LoRa beacon + +optional arguments: + -h, --help show this help message and exit + --ocp OCP, -c OCP Over current protection in mA (45 .. 240 mA) + --sf SF, -s SF Spreading factor (6...12). Default is 7. + --freq FREQ, -f FREQ Frequency + --bw BW, -b BW Bandwidth (one of BW7_8 BW10_4 BW15_6 BW20_8 BW31_25 + BW41_7 BW62_5 BW125 BW250 BW500). Default is BW125. + --cr CODING_RATE, -r CODING_RATE + Coding rate (one of CR4_5 CR4_6 CR4_7 CR4_8). Default + is CR4_5. + --preamble PREAMBLE, -p PREAMBLE + Preamble length. Default is 8. + --single, -S Single transmission + --wait WAIT, -w WAIT Waiting time between transmissions (default is 0s) +``` + + +# Tests + +Execute `test_lora.py` to run a few unit tests. + + +# Contributors + +Please feel free to comment, report issues, or contribute! + +Contact me via my company website [Mayer Analytics](http://mayeranalytics.com) and my private blog +[mcmayer.net](http://mcmayer.net). + +Follow me on twitter [@markuscmayer](https://twitter.com/markuscmayer) and +[@mayeranalytics](https://twitter.com/mayeranalytics). + + +# Roadmap + +95% of functions for the Sx127x LoRa capabilities are implemented. Functions will be added when necessary. +The test coverage is rather low but we intend to change that soon. + +### Semtech SX1272/3 vs. SX1276/7/8/9 +**pySX127x** is not entirely compatible with the 1272. +The 1276 and 1272 chips are different and the interfaces not 100% identical. For example registers 0x26/27. +But the pySX127x library should get you pretty far if you use it with care. Here are the two datasheets: +* [Semtech - SX1276/77/78/79 - 137 MHz to 1020 MHz Low Power Long Range Transceiver](http://www.semtech.com/images/datasheet/sx1276_77_78_79.pdf) +* [Semtech SX1272/73 - 860 MHz to 1020 MHz Low Power Long Range Transceiver](http://www.semtech.com/images/datasheet/sx1272.pdf) + +### HopeRF transceiver ICs ### +HopeRF has a family of LoRa capable transceiver chips [RFM92/95/96/98](http://www.hoperf.com/) +that have identical or almost identical SPI interface as the Semtech SX1276/7/8/9 family. + +### Microchip transceiver IC ### +Likewise Microchip has the chip [RN2483](http://ww1.microchip.com/downloads/en/DeviceDoc/50002346A.pdf) + +The [pySX127x](https://github.com/mayeranalytics/pySX127x) project will therefore be renamed to pyLoRa at some point. + +# LoRaWAN +LoRaWAN is a LPWAN (low power WAN) and, and **pySX127x** has almost no relationship with LoRaWAN. Here we only deal with the interface into the chip(s) that enable the physical layer of LoRaWAN networks. If you need a LoRaWAN implementation have a look at [Jeroennijhof](https://github.com/jeroennijhof)s [LoRaWAN](https://github.com/jeroennijhof/LoRaWAN) which is based on pySX127x. + +By the way, LoRaWAN is what you need when you want to talk to the [TheThingsNetwork](https://www.thethingsnetwork.org/), a "global open LoRaWAN network". The site has a lot of information and links to products and projects. + + +# References + +### Hardware references +* [Semtech SX1276/77/78/79 - 137 MHz to 1020 MHz Low Power Long Range Transceiver](http://www.semtech.com/images/datasheet/sx1276_77_78_79.pdf) +* [Modtronix inAir9](http://modtronix.com/inair9.html) +* [Spidev Documentation](http://tightdev.net/SpiDev_Doc.pdf) +* [Make: Tutorial: Raspberry Pi GPIO Pins and Python](http://makezine.com/projects/tutorial-raspberry-pi-gpio-pins-and-python/) + +### LoRa performance tests +* [Extreme Range Links: LoRa 868 / 900MHz SX1272 LoRa module for Arduino, Raspberry Pi and Intel Galileo](https://www.cooking-hacks.com/documentation/tutorials/extreme-range-lora-sx1272-module-shield-arduino-raspberry-pi-intel-galileo/) +* [UK LoRa versus FSK - 40km LoS (Line of Sight) test!](http://www.instructables.com/id/Introducing-LoRa-/step17/Other-region-tests/) +* [Andreas Spiess LoRaWAN World Record Attempt](https://www.youtube.com/watch?v=adhWIo-7gr4) + +### Spread spectrum modulation theory +* [An Introduction to Spread Spectrum Techniques](http://www.ausairpower.net/OSR-0597.html) +* [Theory of Spread-Spectrum Communications-A Tutorial](http://www.fer.unizg.hr/_download/repository/Theory%20of%20Spread-Spectrum%20Communications-A%20Tutorial.pdf) +(technical paper) + + +# Copyright and License + +© 2015 Mayer Analytics Ltd., All Rights Reserved. + +### Short version +The license is [GNU AGPL](http://www.gnu.org/licenses/agpl-3.0.en.html). + +### Long version +pySX127x is free software: you can redistribute it and/or modify it under the terms of the +GNU Affero General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +pySX127x is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Affero General Public License for more details. + +You can be released from the requirements of the license by obtaining a commercial license. +Such a license is mandatory as soon as you develop commercial activities involving +pySX127x without disclosing the source code of your own applications, or shipping pySX127x with a closed source product. + +You should have received a copy of the GNU General Public License +along with pySX127. If not, see . + +# Other legal boredom +LoRa, LoRaWAN, LoRa Alliance are all trademarks by ... someone. diff --git a/pySX127x/SX127x/LoRa.py b/pySX127x/SX127x/LoRa.py new file mode 100755 index 0000000..15ba492 --- /dev/null +++ b/pySX127x/SX127x/LoRa.py @@ -0,0 +1,951 @@ +""" Defines the SX127x class and a few utility functions. """ +# -*- coding: utf-8 -*- + +# Copyright 2015-2018 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +import sys +from .constants import * +from .board_config import BOARD + + +################################################## Some utility functions ############################################## + +def set_bit(value, index, new_bit): + """ Set the index'th bit of value to new_bit, and return the new value. + :param value: The integer to set the new_bit in + :type value: int + :param index: 0-based index + :param new_bit: New value the bit shall have (0 or 1) + :return: Changed value + :rtype: int + """ + mask = 1 << index + value &= ~mask + if new_bit: + value |= mask + return value + + +def getter(register_address): + """ The getter decorator reads the register content and calls the decorated function to do + post-processing. + :param register_address: Register address + :return: Register value + :rtype: int + """ + def decorator(func): + def wrapper(self): + return func(self, self.spi.xfer([register_address, 0])[1]) + return wrapper + return decorator + + +def setter(register_address): + """ The setter decorator calls the decorated function for pre-processing and + then writes the result to the register + :param register_address: Register address + :return: New register value + :rtype: int + """ + def decorator(func): + def wrapper(self, val): + return self.spi.xfer([register_address | 0x80, func(self, val)])[1] + return wrapper + return decorator + + +############################################### Definition of the LoRa class ########################################### + +class LoRa(object): + + spi = BOARD.SpiDev() # init and get the baord's SPI + mode = None # the mode is backed up here + backup_registers = [] + verbose = True + dio_mapping = [None] * 6 # store the dio mapping here + + def __init__(self, verbose=True, do_calibration=True, calibration_freq=868): + """ Init the object + + Send the device to sleep, read all registers, and do the calibration (if do_calibration=True) + :param verbose: Set the verbosity True/False + :param calibration_freq: call rx_chain_calibration with this parameter. Default is 868 + :param do_calibration: Call rx_chain_calibration, default is True. + """ + self.verbose = verbose + # set the callbacks for DIO0..5 IRQs. + BOARD.add_events(self._dio0, self._dio1, self._dio2, self._dio3, self._dio4, self._dio5) + # set mode to sleep and read all registers + self.set_mode(MODE.SLEEP) + self.backup_registers = self.get_all_registers() + # more setup work: + if do_calibration: + self.rx_chain_calibration(calibration_freq) + # the FSK registers are set up exactly as modtronix do it: + lookup_fsk = [ + #[REG.FSK.LNA , 0x23], + #[REG.FSK.RX_CONFIG , 0x1E], + #[REG.FSK.RSSI_CONFIG , 0xD2], + #[REG.FSK.PREAMBLE_DETECT, 0xAA], + #[REG.FSK.OSC , 0x07], + #[REG.FSK.SYNC_CONFIG , 0x12], + #[REG.FSK.SYNC_VALUE_1 , 0xC1], + #[REG.FSK.SYNC_VALUE_2 , 0x94], + #[REG.FSK.SYNC_VALUE_3 , 0xC1], + #[REG.FSK.PACKET_CONFIG_1, 0xD8], + #[REG.FSK.FIFO_THRESH , 0x8F], + #[REG.FSK.IMAGE_CAL , 0x02], + #[REG.FSK.DIO_MAPPING_1 , 0x00], + #[REG.FSK.DIO_MAPPING_2 , 0x30] + ] + self.set_mode(MODE.FSK_STDBY) + for register_address, value in lookup_fsk: + self.set_register(register_address, value) + self.set_mode(MODE.SLEEP) + # set the dio_ mapping by calling the two get_dio_mapping_* functions + self.get_dio_mapping_1() + self.get_dio_mapping_2() + + + # Overridable functions: + + def on_rx_done(self): + pass + + def on_tx_done(self): + pass + + def on_cad_done(self): + pass + + def on_rx_timeout(self): + pass + + def on_valid_header(self): + pass + + def on_payload_crc_error(self): + pass + + def on_fhss_change_channel(self): + pass + + # Internal callbacks for add_events() + + def _dio0(self, channel): + # DIO0 00: RxDone + # DIO0 01: TxDone + # DIO0 10: CadDone + if self.dio_mapping[0] == 0: + self.on_rx_done() + elif self.dio_mapping[0] == 1: + self.on_tx_done() + elif self.dio_mapping[0] == 2: + self.on_cad_done() + else: + raise RuntimeError("unknown dio0mapping!") + + def _dio1(self, channel): + # DIO1 00: RxTimeout + # DIO1 01: FhssChangeChannel + # DIO1 10: CadDetected + if self.dio_mapping[1] == 0: + self.on_rx_timeout() + elif self.dio_mapping[1] == 1: + self.on_fhss_change_channel() + elif self.dio_mapping[1] == 2: + self.on_CadDetected() + else: + raise RuntimeError("unknown dio1mapping!") + + def _dio2(self, channel): + # DIO2 00: FhssChangeChannel + # DIO2 01: FhssChangeChannel + # DIO2 10: FhssChangeChannel + self.on_fhss_change_channel() + + def _dio3(self, channel): + # DIO3 00: CadDone + # DIO3 01: ValidHeader + # DIO3 10: PayloadCrcError + if self.dio_mapping[3] == 0: + self.on_cad_done() + elif self.dio_mapping[3] == 1: + self.on_valid_header() + elif self.dio_mapping[3] == 2: + self.on_payload_crc_error() + else: + raise RuntimeError("unknown dio3 mapping!") + + def _dio4(self, channel): + raise RuntimeError("DIO4 is not used") + + def _dio5(self, channel): + raise RuntimeError("DIO5 is not used") + + # All the set/get/read/write functions + + def get_mode(self): + """ Get the mode + :return: New mode + """ + self.mode = self.spi.xfer([REG.LORA.OP_MODE, 0])[1] + return self.mode + + def set_mode(self, mode): + """ Set the mode + :param mode: Set the mode. Use constants.MODE class + :return: New mode + """ + # the mode is backed up in self.mode + if mode == self.mode: + return mode + if self.verbose: + sys.stderr.write("Mode <- %s\n" % MODE.lookup[mode]) + self.mode = mode + return self.spi.xfer([REG.LORA.OP_MODE | 0x80, mode])[1] + + def write_payload(self, payload): + """ Get FIFO ready for TX: Set FifoAddrPtr to FifoTxBaseAddr. The transceiver is put into STDBY mode. + :param payload: Payload to write (list) + :return: Written payload + """ + payload_size = len(payload) + self.set_payload_length(payload_size) + + self.set_mode(MODE.STDBY) + base_addr = self.get_fifo_tx_base_addr() + self.set_fifo_addr_ptr(base_addr) + return self.spi.xfer([REG.LORA.FIFO | 0x80] + payload)[1:] + + def reset_ptr_rx(self): + """ Get FIFO ready for RX: Set FifoAddrPtr to FifoRxBaseAddr. The transceiver is put into STDBY mode. """ + self.set_mode(MODE.STDBY) + base_addr = self.get_fifo_rx_base_addr() + self.set_fifo_addr_ptr(base_addr) + + def rx_is_good(self): + """ Check the IRQ flags for RX errors + :return: True if no errors + :rtype: bool + """ + flags = self.get_irq_flags() + return not any([flags[s] for s in ['valid_header', 'crc_error', 'rx_done', 'rx_timeout']]) + + def read_payload(self , nocheck = False): + """ Read the payload from FIFO + :param nocheck: If True then check rx_is_good() + :return: Payload + :rtype: list[int] + """ + if not nocheck and not self.rx_is_good(): + return None + rx_nb_bytes = self.get_rx_nb_bytes() + fifo_rx_current_addr = self.get_fifo_rx_current_addr() + self.set_fifo_addr_ptr(fifo_rx_current_addr) + payload = self.spi.xfer([REG.LORA.FIFO] + [0] * rx_nb_bytes)[1:] + return payload + + def get_freq(self): + """ Get the frequency (MHz) + :return: Frequency in MHz + :rtype: float + """ + msb, mid, lsb = self.spi.xfer([REG.LORA.FR_MSB, 0, 0, 0])[1:] + f = lsb + 256*(mid + 256*msb) + return f / 16384. + + def set_freq(self, f): + """ Set the frequency (MHz) + :param f: Frequency in MHz + "type f: float + :return: New register settings (3 bytes [msb, mid, lsb]) + :rtype: list[int] + """ + assert self.mode == MODE.SLEEP or self.mode == MODE.STDBY or self.mode == MODE.FSK_STDBY + i = int(f * 16384.) # choose floor + msb = i // 65536 + i -= msb * 65536 + mid = i // 256 + i -= mid * 256 + lsb = i + return self.spi.xfer([REG.LORA.FR_MSB | 0x80, msb, mid, lsb]) + + def get_pa_config(self, convert_dBm=False): + v = self.spi.xfer([REG.LORA.PA_CONFIG, 0])[1] + pa_select = v >> 7 + max_power = v >> 4 & 0b111 + output_power = v & 0b1111 + if convert_dBm: + max_power = max_power * .6 + 10.8 + output_power = max_power - (15 - output_power) + return dict( + pa_select = pa_select, + max_power = max_power, + output_power = output_power + ) + + def set_pa_config(self, pa_select=None, max_power=None, output_power=None): + """ Configure the PA + :param pa_select: Selects PA output pin, 0->RFO, 1->PA_BOOST + :param max_power: Select max output power Pmax=10.8+0.6*MaxPower + :param output_power: Output power Pout=Pmax-(15-OutputPower) if PaSelect = 0, + Pout=17-(15-OutputPower) if PaSelect = 1 (PA_BOOST pin) + :return: new register value + """ + loc = locals() + current = self.get_pa_config() + loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} + val = (loc['pa_select'] << 7) | (loc['max_power'] << 4) | (loc['output_power']) + return self.spi.xfer([REG.LORA.PA_CONFIG | 0x80, val])[1] + + @getter(REG.LORA.PA_RAMP) + def get_pa_ramp(self, val): + return val & 0b1111 + + @setter(REG.LORA.PA_RAMP) + def set_pa_ramp(self, val): + return val & 0b1111 + + def get_ocp(self, convert_mA=False): + v = self.spi.xfer([REG.LORA.OCP, 0])[1] + ocp_on = v >> 5 & 0x01 + ocp_trim = v & 0b11111 + if convert_mA: + if ocp_trim <= 15: + ocp_trim = 45. + 5. * ocp_trim + elif ocp_trim <= 27: + ocp_trim = -30. + 10. * ocp_trim + else: + assert ocp_trim <= 27 + return dict( + ocp_on = ocp_on, + ocp_trim = ocp_trim + ) + + def set_ocp_trim(self, I_mA): + assert(I_mA >= 45 and I_mA <= 240) + ocp_on = self.spi.xfer([REG.LORA.OCP, 0])[1] >> 5 & 0x01 + if I_mA <= 120: + v = int(round((I_mA-45.)/5.)) + else: + v = int(round((I_mA+30.)/10.)) + v = set_bit(v, 5, ocp_on) + return self.spi.xfer([REG.LORA.OCP | 0x80, v])[1] + + def get_lna(self): + v = self.spi.xfer([REG.LORA.LNA, 0])[1] + return dict( + lna_gain = v >> 5, + lna_boost_lf = v >> 3 & 0b11, + lna_boost_hf = v & 0b11 + ) + + def set_lna(self, lna_gain=None, lna_boost_lf=None, lna_boost_hf=None): + assert lna_boost_hf is None or lna_boost_hf == 0b00 or lna_boost_hf == 0b11 + self.set_mode(MODE.STDBY) + if lna_gain is not None: + # Apparently agc_auto_on must be 0 in order to set lna_gain + self.set_agc_auto_on(lna_gain == GAIN.NOT_USED) + loc = locals() + current = self.get_lna() + loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} + val = (loc['lna_gain'] << 5) | (loc['lna_boost_lf'] << 3) | (loc['lna_boost_hf']) + retval = self.spi.xfer([REG.LORA.LNA | 0x80, val])[1] + if lna_gain is not None: + # agc_auto_on must track lna_gain: GAIN=NOT_USED -> agc_auto=ON, otherwise =OFF + self.set_agc_auto_on(lna_gain == GAIN.NOT_USED) + return retval + + def set_lna_gain(self, lna_gain): + self.set_lna(lna_gain=lna_gain) + + def get_fifo_addr_ptr(self): + return self.spi.xfer([REG.LORA.FIFO_ADDR_PTR, 0])[1] + + def set_fifo_addr_ptr(self, ptr): + return self.spi.xfer([REG.LORA.FIFO_ADDR_PTR | 0x80, ptr])[1] + + def get_fifo_tx_base_addr(self): + return self.spi.xfer([REG.LORA.FIFO_TX_BASE_ADDR, 0])[1] + + def set_fifo_tx_base_addr(self, ptr): + return self.spi.xfer([REG.LORA.FIFO_TX_BASE_ADDR | 0x80, ptr])[1] + + def get_fifo_rx_base_addr(self): + return self.spi.xfer([REG.LORA.FIFO_RX_BASE_ADDR, 0])[1] + + def set_fifo_rx_base_addr(self, ptr): + return self.spi.xfer([REG.LORA.FIFO_RX_BASE_ADDR | 0x80, ptr])[1] + + def get_fifo_rx_current_addr(self): + return self.spi.xfer([REG.LORA.FIFO_RX_CURR_ADDR, 0])[1] + + def get_fifo_rx_byte_addr(self): + return self.spi.xfer([REG.LORA.FIFO_RX_BYTE_ADDR, 0])[1] + + def get_irq_flags_mask(self): + v = self.spi.xfer([REG.LORA.IRQ_FLAGS_MASK, 0])[1] + return dict( + rx_timeout = v >> 7 & 0x01, + rx_done = v >> 6 & 0x01, + crc_error = v >> 5 & 0x01, + valid_header = v >> 4 & 0x01, + tx_done = v >> 3 & 0x01, + cad_done = v >> 2 & 0x01, + fhss_change_ch = v >> 1 & 0x01, + cad_detected = v >> 0 & 0x01, + ) + + def set_irq_flags_mask(self, + rx_timeout=None, rx_done=None, crc_error=None, valid_header=None, tx_done=None, + cad_done=None, fhss_change_ch=None, cad_detected=None): + loc = locals() + v = self.spi.xfer([REG.LORA.IRQ_FLAGS_MASK, 0])[1] + for i, s in enumerate(['cad_detected', 'fhss_change_ch', 'cad_done', 'tx_done', 'valid_header', + 'crc_error', 'rx_done', 'rx_timeout']): + this_bit = locals()[s] + if this_bit is not None: + v = set_bit(v, i, this_bit) + return self.spi.xfer([REG.LORA.IRQ_FLAGS_MASK | 0x80, v])[1] + + def get_irq_flags(self): + v = self.spi.xfer([REG.LORA.IRQ_FLAGS, 0])[1] + return dict( + rx_timeout = v >> 7 & 0x01, + rx_done = v >> 6 & 0x01, + crc_error = v >> 5 & 0x01, + valid_header = v >> 4 & 0x01, + tx_done = v >> 3 & 0x01, + cad_done = v >> 2 & 0x01, + fhss_change_ch = v >> 1 & 0x01, + cad_detected = v >> 0 & 0x01, + ) + + def set_irq_flags(self, + rx_timeout=None, rx_done=None, crc_error=None, valid_header=None, tx_done=None, + cad_done=None, fhss_change_ch=None, cad_detected=None): + v = self.spi.xfer([REG.LORA.IRQ_FLAGS, 0])[1] + for i, s in enumerate(['cad_detected', 'fhss_change_ch', 'cad_done', 'tx_done', 'valid_header', + 'crc_error', 'rx_done', 'rx_timeout']): + this_bit = locals()[s] + if this_bit is not None: + v = set_bit(v, i, this_bit) + return self.spi.xfer([REG.LORA.IRQ_FLAGS | 0x80, v])[1] + + def clear_irq_flags(self, + RxTimeout=None, RxDone=None, PayloadCrcError=None, + ValidHeader=None, TxDone=None, CadDone=None, + FhssChangeChannel=None, CadDetected=None): + v = 0 + for i, s in enumerate(['CadDetected', 'FhssChangeChannel', 'CadDone', + 'TxDone', 'ValidHeader', 'PayloadCrcError', + 'RxDone', 'RxTimeout']): + this_bit = locals()[s] + if this_bit is not None: + v = set_bit(v, eval('MASK.IRQ_FLAGS.' + s), this_bit) + return self.spi.xfer([REG.LORA.IRQ_FLAGS | 0x80, v])[1] + + + def get_rx_nb_bytes(self): + return self.spi.xfer([REG.LORA.RX_NB_BYTES, 0])[1] + + def get_rx_header_cnt(self): + msb, lsb = self.spi.xfer([REG.LORA.RX_HEADER_CNT_MSB, 0, 0])[1:] + return lsb + 256 * msb + + def get_rx_packet_cnt(self): + msb, lsb = self.spi.xfer([REG.LORA.RX_PACKET_CNT_MSB, 0, 0])[1:] + return lsb + 256 * msb + + def get_modem_status(self): + status = self.spi.xfer([REG.LORA.MODEM_STAT, 0])[1] + return dict( + rx_coding_rate = status >> 5 & 0x03, + modem_clear = status >> 4 & 0x01, + header_info_valid = status >> 3 & 0x01, + rx_ongoing = status >> 2 & 0x01, + signal_sync = status >> 1 & 0x01, + signal_detected = status >> 0 & 0x01 + ) + + def get_pkt_snr_value(self): + v = self.spi.xfer([REG.LORA.PKT_SNR_VALUE, 0])[1] + return (float(v-256) if v > 127 else float(v)) / 4. + + def get_pkt_rssi_value(self): + v = self.spi.xfer([REG.LORA.PKT_RSSI_VALUE, 0])[1] + return v - (164 if BOARD.low_band else 157) # See datasheet 5.5.5. p. 87 + + def get_rssi_value(self): + v = self.spi.xfer([REG.LORA.RSSI_VALUE, 0])[1] + return v - (164 if BOARD.low_band else 157) # See datasheet 5.5.5. p. 87 + + def get_hop_channel(self): + v = self.spi.xfer([REG.LORA.HOP_CHANNEL, 0])[1] + return dict( + pll_timeout = v >> 7, + crc_on_payload = v >> 6 & 0x01, + fhss_present_channel = v >> 5 & 0b111111 + ) + + def get_modem_config_1(self): + val = self.spi.xfer([REG.LORA.MODEM_CONFIG_1, 0])[1] + return dict( + bw = val >> 4 & 0x0F, + coding_rate = val >> 1 & 0x07, + implicit_header_mode = val & 0x01 + ) + + def set_modem_config_1(self, bw=None, coding_rate=None, implicit_header_mode=None): + loc = locals() + current = self.get_modem_config_1() + loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} + val = loc['implicit_header_mode'] | (loc['coding_rate'] << 1) | (loc['bw'] << 4) + return self.spi.xfer([REG.LORA.MODEM_CONFIG_1 | 0x80, val])[1] + + def set_bw(self, bw): + """ Set the bandwidth 0=7.8kHz ... 9=500kHz + :param bw: A number 0,2,3,...,9 + :return: + """ + self.set_modem_config_1(bw=bw) + + def set_coding_rate(self, coding_rate): + """ Set the coding rate 4/5, 4/6, 4/7, 4/8 + :param coding_rate: A number 1,2,3,4 + :return: New register value + """ + self.set_modem_config_1(coding_rate=coding_rate) + + def set_implicit_header_mode(self, implicit_header_mode): + self.set_modem_config_1(implicit_header_mode=implicit_header_mode) + + def get_modem_config_2(self, include_symb_timout_lsb=False): + val = self.spi.xfer([REG.LORA.MODEM_CONFIG_2, 0])[1] + d = dict( + spreading_factor = val >> 4 & 0x0F, + tx_cont_mode = val >> 3 & 0x01, + rx_crc = val >> 2 & 0x01, + ) + if include_symb_timout_lsb: + d['symb_timout_lsb'] = val & 0x03 + return d + + def set_modem_config_2(self, spreading_factor=None, tx_cont_mode=None, rx_crc=None): + loc = locals() + # RegModemConfig2 contains the SymbTimout MSB bits. We tack the back on when writing this register. + current = self.get_modem_config_2(include_symb_timout_lsb=True) + loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} + val = (loc['spreading_factor'] << 4) | (loc['tx_cont_mode'] << 3) | (loc['rx_crc'] << 2) | current['symb_timout_lsb'] + return self.spi.xfer([REG.LORA.MODEM_CONFIG_2 | 0x80, val])[1] + + def set_spreading_factor(self, spreading_factor): + self.set_modem_config_2(spreading_factor=spreading_factor) + + def set_rx_crc(self, rx_crc): + self.set_modem_config_2(rx_crc=rx_crc) + + def get_modem_config_3(self): + val = self.spi.xfer([REG.LORA.MODEM_CONFIG_3, 0])[1] + return dict( + low_data_rate_optim = val >> 3 & 0x01, + agc_auto_on = val >> 2 & 0x01 + ) + + def set_modem_config_3(self, low_data_rate_optim=None, agc_auto_on=None): + loc = locals() + current = self.get_modem_config_3() + loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} + val = (loc['low_data_rate_optim'] << 3) | (loc['agc_auto_on'] << 2) + return self.spi.xfer([REG.LORA.MODEM_CONFIG_3 | 0x80, val])[1] + + @setter(REG.LORA.INVERT_IQ) + def set_invert_iq(self, invert): + """ Invert the LoRa I and Q signals + :param invert: 0: normal mode, 1: I and Q inverted + :return: New value of register + """ + return 0x27 | (invert & 0x01) << 6 + + @getter(REG.LORA.INVERT_IQ) + def get_invert_iq(self, val): + """ Get the invert the I and Q setting + :return: 0: normal mode, 1: I and Q inverted + """ + return (val >> 6) & 0x01 + + def get_agc_auto_on(self): + return self.get_modem_config_3()['agc_auto_on'] + + def set_agc_auto_on(self, agc_auto_on): + self.set_modem_config_3(agc_auto_on=agc_auto_on) + + def get_low_data_rate_optim(self): + return self.set_modem_config_3()['low_data_rate_optim'] + + def set_low_data_rate_optim(self, low_data_rate_optim): + self.set_modem_config_3(low_data_rate_optim=low_data_rate_optim) + + def get_symb_timeout(self): + SYMB_TIMEOUT_MSB = REG.LORA.MODEM_CONFIG_2 + msb, lsb = self.spi.xfer([SYMB_TIMEOUT_MSB, 0, 0])[1:] # the MSB bits are stored in REG.LORA.MODEM_CONFIG_2 + msb = msb & 0b11 + return lsb + 256 * msb + + def set_symb_timeout(self, timeout): + bkup_reg_modem_config_2 = self.spi.xfer([REG.LORA.MODEM_CONFIG_2, 0])[1] + msb = timeout >> 8 & 0b11 # bits 8-9 + lsb = timeout - 256 * msb # bits 0-7 + reg_modem_config_2 = bkup_reg_modem_config_2 & 0xFC | msb # bits 2-7 of bkup_reg_modem_config_2 ORed with the two msb bits + old_msb = self.spi.xfer([REG.LORA.MODEM_CONFIG_2 | 0x80, reg_modem_config_2])[1] & 0x03 + old_lsb = self.spi.xfer([REG.LORA.SYMB_TIMEOUT_LSB | 0x80, lsb])[1] + return old_lsb + 256 * old_msb + + def get_preamble(self): + msb, lsb = self.spi.xfer([REG.LORA.PREAMBLE_MSB, 0, 0])[1:] + return lsb + 256 * msb + + def set_preamble(self, preamble): + msb = preamble >> 8 + lsb = preamble - msb * 256 + old_msb, old_lsb = self.spi.xfer([REG.LORA.PREAMBLE_MSB | 0x80, msb, lsb])[1:] + return old_lsb + 256 * old_msb + + @getter(REG.LORA.PAYLOAD_LENGTH) + def get_payload_length(self, val): + return val + + @setter(REG.LORA.PAYLOAD_LENGTH) + def set_payload_length(self, payload_length): + return payload_length + + @getter(REG.LORA.MAX_PAYLOAD_LENGTH) + def get_max_payload_length(self, val): + return val + + @setter(REG.LORA.MAX_PAYLOAD_LENGTH) + def set_max_payload_length(self, max_payload_length): + return max_payload_length + + @getter(REG.LORA.HOP_PERIOD) + def get_hop_period(self, val): + return val + + @setter(REG.LORA.HOP_PERIOD) + def set_hop_period(self, hop_period): + return hop_period + + def get_fei(self): + msb, mid, lsb = self.spi.xfer([REG.LORA.FEI_MSB, 0, 0, 0])[1:] + msb &= 0x0F + freq_error = lsb + 256 * (mid + 256 * msb) + return freq_error + + @getter(REG.LORA.DETECT_OPTIMIZE) + def get_detect_optimize(self, val): + """ Get LoRa detection optimize setting + :return: detection optimize setting 0x03: SF7-12, 0x05: SF6 + + """ + return val & 0b111 + + @setter(REG.LORA.DETECT_OPTIMIZE) + def set_detect_optimize(self, detect_optimize): + """ Set LoRa detection optimize + :param detect_optimize 0x03: SF7-12, 0x05: SF6 + :return: New register value + """ + assert detect_optimize == 0x03 or detect_optimize == 0x05 + return detect_optimize & 0b111 + + @getter(REG.LORA.DETECTION_THRESH) + def get_detection_threshold(self, val): + """ Get LoRa detection threshold setting + :return: detection threshold 0x0A: SF7-12, 0x0C: SF6 + + """ + return val + + @setter(REG.LORA.DETECTION_THRESH) + def set_detection_threshold(self, detect_threshold): + """ Set LoRa detection optimize + :param detect_threshold 0x0A: SF7-12, 0x0C: SF6 + :return: New register value + """ + assert detect_threshold == 0x0A or detect_threshold == 0x0C + return detect_threshold + + @getter(REG.LORA.SYNC_WORD) + def get_sync_word(self, sync_word): + return sync_word + + @setter(REG.LORA.SYNC_WORD) + def set_sync_word(self, sync_word): + return sync_word + + @getter(REG.LORA.DIO_MAPPING_1) + def get_dio_mapping_1(self, mapping): + """ Get mapping of pins DIO0 to DIO3. Object variable dio_mapping will be set. + :param mapping: Register value + :type mapping: int + :return: Value of the mapping list + :rtype: list[int] + """ + self.dio_mapping = [mapping>>6 & 0x03, mapping>>4 & 0x03, mapping>>2 & 0x03, mapping>>0 & 0x03] \ + + self.dio_mapping[4:6] + return self.dio_mapping + + @setter(REG.LORA.DIO_MAPPING_1) + def set_dio_mapping_1(self, mapping): + """ Set mapping of pins DIO0 to DIO3. Object variable dio_mapping will be set. + :param mapping: Register value + :type mapping: int + :return: New value of the register + :rtype: int + """ + self.dio_mapping = [mapping>>6 & 0x03, mapping>>4 & 0x03, mapping>>2 & 0x03, mapping>>0 & 0x03] \ + + self.dio_mapping[4:6] + return mapping + + @getter(REG.LORA.DIO_MAPPING_2) + def get_dio_mapping_2(self, mapping): + """ Get mapping of pins DIO4 to DIO5. Object variable dio_mapping will be set. + :param mapping: Register value + :type mapping: int + :return: Value of the mapping list + :rtype: list[int] + """ + self.dio_mapping = self.dio_mapping[0:4] + [mapping>>6 & 0x03, mapping>>4 & 0x03] + return self.dio_mapping + + @setter(REG.LORA.DIO_MAPPING_2) + def set_dio_mapping_2(self, mapping): + """ Set mapping of pins DIO4 to DIO5. Object variable dio_mapping will be set. + :param mapping: Register value + :type mapping: int + :return: New value of the register + :rtype: int + """ + assert mapping & 0b00001110 == 0 + self.dio_mapping = self.dio_mapping[0:4] + [mapping>>6 & 0x03, mapping>>4 & 0x03] + return mapping + + def get_dio_mapping(self): + """ Utility function that returns the list of current DIO mappings. Object variable dio_mapping will be set. + :return: List of current DIO mappings + :rtype: list[int] + """ + self.get_dio_mapping_1() + return self.get_dio_mapping_2() + + def set_dio_mapping(self, mapping): + """ Utility function that returns the list of current DIO mappings. Object variable dio_mapping will be set. + :param mapping: DIO mapping list + :type mapping: list[int] + :return: New DIO mapping list + :rtype: list[int] + """ + mapping_1 = (mapping[0] & 0x03) << 6 | (mapping[1] & 0x03) << 4 | (mapping[2] & 0x3) << 2 | mapping[3] & 0x3 + mapping_2 = (mapping[4] & 0x03) << 6 | (mapping[5] & 0x03) << 4 + self.set_dio_mapping_1(mapping_1) + return self.set_dio_mapping_2(mapping_2) + + @getter(REG.LORA.VERSION) + def get_version(self, version): + """ Version code of the chip. + Bits 7-4 give the full revision number; bits 3-0 give the metal mask revision number. + :return: Version code + :rtype: int + """ + return version + + @getter(REG.LORA.TCXO) + def get_tcxo(self, tcxo): + """ Get TCXO or XTAL input setting + 0 -> "XTAL": Crystal Oscillator with external Crystal + 1 -> "TCXO": External clipped sine TCXO AC-connected to XTA pin + :param tcxo: 1=TCXO or 0=XTAL input setting + :return: TCXO or XTAL input setting + :type: int (0 or 1) + """ + return tcxo & 0b00010000 + + @setter(REG.LORA.TCXO) + def set_tcxo(self, tcxo): + """ Make TCXO or XTAL input setting. + 0 -> "XTAL": Crystal Oscillator with external Crystal + 1 -> "TCXO": External clipped sine TCXO AC-connected to XTA pin + :param tcxo: 1=TCXO or 0=XTAL input setting + :return: new TCXO or XTAL input setting + """ + return (tcxo >= 1) << 4 | 0x09 # bits 0-3 must be 0b1001 + + @getter(REG.LORA.PA_DAC) + def get_pa_dac(self, pa_dac): + """ Enables the +20dBm option on PA_BOOST pin + False -> Default value + True -> +20dBm on PA_BOOST when OutputPower=1111 + :return: True/False if +20dBm option on PA_BOOST on/off + :rtype: bool + """ + pa_dac &= 0x07 # only bits 0-2 + if pa_dac == 0x04: + return False + elif pa_dac == 0x07: + return True + else: + raise RuntimeError("Bad PA_DAC value %s" % hex(pa_dac)) + + @setter(REG.LORA.PA_DAC) + def set_pa_dac(self, pa_dac): + """ Enables the +20dBm option on PA_BOOST pin + False -> Default value + True -> +20dBm on PA_BOOST when OutputPower=1111 + :param pa_dac: 1/0 if +20dBm option on PA_BOOST on/off + :return: New pa_dac register value + :rtype: int + """ + return 0x87 if pa_dac else 0x84 + + def rx_chain_calibration(self, freq=868.): + """ Run the image calibration (see Semtech documentation section 4.2.3.8) + :param freq: Frequency for the HF calibration + :return: None + """ + # backup some registers + op_mode_bkup = self.get_mode() + pa_config_bkup = self.get_register(REG.LORA.PA_CONFIG) + freq_bkup = self.get_freq() + # for image calibration device must be in FSK standby mode + self.set_mode(MODE.FSK_STDBY) + # cut the PA + self.set_register(REG.LORA.PA_CONFIG, 0x00) + # calibration for the LF band + image_cal = (self.get_register(REG.FSK.IMAGE_CAL) & 0xBF) | 0x40 + self.set_register(REG.FSK.IMAGE_CAL, image_cal) + while (self.get_register(REG.FSK.IMAGE_CAL) & 0x20) == 0x20: + pass + # Set a Frequency in HF band + self.set_freq(freq) + # calibration for the HF band + image_cal = (self.get_register(REG.FSK.IMAGE_CAL) & 0xBF) | 0x40 + self.set_register(REG.FSK.IMAGE_CAL, image_cal) + while (self.get_register(REG.FSK.IMAGE_CAL) & 0x20) == 0x20: + pass + # put back the saved parameters + self.set_mode(op_mode_bkup) + self.set_register(REG.LORA.PA_CONFIG, pa_config_bkup) + self.set_freq(freq_bkup) + + def dump_registers(self): + """ Returns a list of [reg_addr, reg_name, reg_value] tuples. Chip is put into mode SLEEP. + :return: List of [reg_addr, reg_name, reg_value] tuples + :rtype: list[tuple] + """ + self.set_mode(MODE.SLEEP) + values = self.get_all_registers() + skip_set = set([REG.LORA.FIFO]) + result_list = [] + for i, s in REG.LORA.lookup.iteritems(): + if i in skip_set: + continue + v = values[i] + result_list.append((i, s, v)) + return result_list + + def get_register(self, register_address): + return self.spi.xfer([register_address & 0x7F, 0])[1] + + def set_register(self, register_address, val): + return self.spi.xfer([register_address | 0x80, val])[1] + + def get_all_registers(self): + # read all registers + reg = [0] + self.spi.xfer([1]+[0]*0x3E)[1:] + self.mode = reg[1] + return reg + + def __del__(self): + self.set_mode(MODE.SLEEP) + if self.verbose: + sys.stderr.write("MODE=SLEEP\n") + + def __str__(self): + # don't use __str__ while in any mode other that SLEEP or STDBY + assert(self.mode == MODE.SLEEP or self.mode == MODE.STDBY) + + onoff = lambda i: 'ON' if i else 'OFF' + f = self.get_freq() + cfg1 = self.get_modem_config_1() + cfg2 = self.get_modem_config_2() + cfg3 = self.get_modem_config_3() + pa_config = self.get_pa_config(convert_dBm=True) + ocp = self.get_ocp(convert_mA=True) + lna = self.get_lna() + s = "SX127x LoRa registers:\n" + s += " mode %s\n" % MODE.lookup[self.get_mode()] + s += " freq %f MHz\n" % f + s += " coding_rate %s\n" % CODING_RATE.lookup[cfg1['coding_rate']] + s += " bw %s\n" % BW.lookup[cfg1['bw']] + s += " spreading_factor %s chips/symb\n" % (1 << cfg2['spreading_factor']) + s += " implicit_hdr_mode %s\n" % onoff(cfg1['implicit_header_mode']) + s += " rx_payload_crc %s\n" % onoff(cfg2['rx_crc']) + s += " tx_cont_mode %s\n" % onoff(cfg2['tx_cont_mode']) + s += " preamble %d\n" % self.get_preamble() + s += " low_data_rate_opti %s\n" % onoff(cfg3['low_data_rate_optim']) + s += " agc_auto_on %s\n" % onoff(cfg3['agc_auto_on']) + s += " symb_timeout %s\n" % self.get_symb_timeout() + s += " freq_hop_period %s\n" % self.get_hop_period() + s += " hop_channel %s\n" % self.get_hop_channel() + s += " payload_length %s\n" % self.get_payload_length() + s += " max_payload_length %s\n" % self.get_max_payload_length() + s += " irq_flags_mask %s\n" % self.get_irq_flags_mask() + s += " irq_flags %s\n" % self.get_irq_flags() + s += " rx_nb_byte %d\n" % self.get_rx_nb_bytes() + s += " rx_header_cnt %d\n" % self.get_rx_header_cnt() + s += " rx_packet_cnt %d\n" % self.get_rx_packet_cnt() + s += " pkt_snr_value %f\n" % self.get_pkt_snr_value() + s += " pkt_rssi_value %d\n" % self.get_pkt_rssi_value() + s += " rssi_value %d\n" % self.get_rssi_value() + s += " fei %d\n" % self.get_fei() + s += " pa_select %s\n" % PA_SELECT.lookup[pa_config['pa_select']] + s += " max_power %f dBm\n" % pa_config['max_power'] + s += " output_power %f dBm\n" % pa_config['output_power'] + s += " ocp %s\n" % onoff(ocp['ocp_on']) + s += " ocp_trim %f mA\n" % ocp['ocp_trim'] + s += " lna_gain %s\n" % GAIN.lookup[lna['lna_gain']] + s += " lna_boost_lf %s\n" % bin(lna['lna_boost_lf']) + s += " lna_boost_hf %s\n" % bin(lna['lna_boost_hf']) + s += " detect_optimize %#02x\n" % self.get_detect_optimize() + s += " detection_thresh %#02x\n" % self.get_detection_threshold() + s += " sync_word %#02x\n" % self.get_sync_word() + s += " dio_mapping 0..5 %s\n" % self.get_dio_mapping() + s += " tcxo %s\n" % ['XTAL', 'TCXO'][self.get_tcxo()] + s += " pa_dac %s\n" % ['default', 'PA_BOOST'][self.get_pa_dac()] + s += " fifo_addr_ptr %#02x\n" % self.get_fifo_addr_ptr() + s += " fifo_tx_base_addr %#02x\n" % self.get_fifo_tx_base_addr() + s += " fifo_rx_base_addr %#02x\n" % self.get_fifo_rx_base_addr() + s += " fifo_rx_curr_addr %#02x\n" % self.get_fifo_rx_current_addr() + s += " fifo_rx_byte_addr %#02x\n" % self.get_fifo_rx_byte_addr() + s += " status %s\n" % self.get_modem_status() + s += " version %#02x\n" % self.get_version() + return s diff --git a/pySX127x/SX127x/LoRaArgumentParser.py b/pySX127x/SX127x/LoRaArgumentParser.py new file mode 100755 index 0000000..c9da469 --- /dev/null +++ b/pySX127x/SX127x/LoRaArgumentParser.py @@ -0,0 +1,76 @@ +""" Defines LoRaArgumentParser which extends argparse.ArgumentParser with standard config parameters for the SX127x. """ +# -*- coding: utf-8 -*- + +# Copyright 2018 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +import argparse + + +class LoRaArgumentParser(argparse.ArgumentParser): + """ This class extends argparse.ArgumentParser. + Some commonly used LoRa config parameters are defined + * ocp + * spreading factor + * frequency + * bandwidth + * preamble + Call the parse_args with an additional parameter referencing a LoRa object. The args will be used to configure + the LoRa. + """ + + bw_lookup = dict(BW7_8=0, BW10_4=1, BW15_6=2, BW20_8=3, BW31_25=4, BW41_7=5, BW62_5=6, BW125=7, BW250=8, BW500=9) + cr_lookup = dict(CR4_5=1, CR4_6=2,CR4_7=3,CR4_8=4) + + def __init__(self, description): + argparse.ArgumentParser.__init__(self, description=description) + self.add_argument('--ocp', '-c', dest='ocp', default=100, action="store", type=float, + help="Over current protection in mA (45 .. 240 mA)") + self.add_argument('--sf', '-s', dest='sf', default=7, action="store", type=int, + help="Spreading factor (6...12). Default is 7.") + self.add_argument('--freq', '-f', dest='freq', default=869., action="store", type=float, + help="Frequency") + self.add_argument('--bw', '-b', dest='bw', default='BW125', action="store", type=str, + help="Bandwidth (one of BW7_8 BW10_4 BW15_6 BW20_8 BW31_25 BW41_7 BW62_5 BW125 BW250 BW500).\nDefault is BW125.") + self.add_argument('--cr', '-r', dest='coding_rate', default='CR4_5', action="store", type=str, + help="Coding rate (one of CR4_5 CR4_6 CR4_7 CR4_8).\nDefault is CR4_5.") + self.add_argument('--preamble', '-p', dest='preamble', default=8, action="store", type=int, + help="Preamble length. Default is 8.") + + def parse_args(self, lora): + """ Parse the args, perform some sanity checks and configure the LoRa accordingly. + :param lora: Reference to LoRa object + :return: args + """ + args = argparse.ArgumentParser.parse_args(self) + args.bw = self.bw_lookup.get(args.bw, None) + args.coding_rate = self.cr_lookup.get(args.coding_rate, None) + # some sanity checks + assert(args.bw is not None) + assert(args.coding_rate is not None) + assert(args.sf >=6 and args.sf <= 12) + # set the LoRa object + lora.set_freq(args.freq) + lora.set_preamble(args.preamble) + lora.set_spreading_factor(args.sf) + lora.set_bw(args.bw) + lora.set_coding_rate(args.coding_rate) + lora.set_ocp_trim(args.ocp) + return args diff --git a/pySX127x/SX127x/__init__.py b/pySX127x/SX127x/__init__.py new file mode 100755 index 0000000..78c5d36 --- /dev/null +++ b/pySX127x/SX127x/__init__.py @@ -0,0 +1 @@ +__all__ = ['SX127x'] diff --git a/pySX127x/SX127x/board_config.py b/pySX127x/SX127x/board_config.py new file mode 100755 index 0000000..14dae8e --- /dev/null +++ b/pySX127x/SX127x/board_config.py @@ -0,0 +1,134 @@ +""" Defines the BOARD class that contains the board pin mappings and RF module HF/LF info. """ +# -*- coding: utf-8 -*- + +# Copyright 2015-2018 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +import RPi.GPIO as GPIO +import spidev + +import time + + +class BOARD: + """ Board initialisation/teardown and pin configuration is kept here. + Also, information about the RF module is kept here. + This is the Raspberry Pi board with one LED and a modtronix inAir9B. + """ + # Note that the BCOM numbering for the GPIOs is used. + DIO0 = 22 # RaspPi GPIO 22 + DIO1 = 23 # RaspPi GPIO 23 + DIO2 = 24 # RaspPi GPIO 24 + DIO3 = 25 # RaspPi GPIO 25 + LED = 18 # RaspPi GPIO 18 connects to the LED on the proto shield + SWITCH = 4 # RaspPi GPIO 4 connects to a switch + + # The spi object is kept here + spi = None + + # tell pySX127x here whether the attached RF module uses low-band (RF*_LF pins) or high-band (RF*_HF pins). + # low band (called band 1&2) are 137-175 and 410-525 + # high band (called band 3) is 862-1020 + low_band = True + + @staticmethod + def setup(): + """ Configure the Raspberry GPIOs + :rtype : None + """ + GPIO.setmode(GPIO.BCM) + # LED + GPIO.setup(BOARD.LED, GPIO.OUT) + GPIO.output(BOARD.LED, 0) + # switch + GPIO.setup(BOARD.SWITCH, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + # DIOx + for gpio_pin in [BOARD.DIO0, BOARD.DIO1, BOARD.DIO2, BOARD.DIO3]: + GPIO.setup(gpio_pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + # blink 2 times to signal the board is set up + BOARD.blink(.1, 2) + + @staticmethod + def teardown(): + """ Cleanup GPIO and SpiDev """ + GPIO.cleanup() + BOARD.spi.close() + + @staticmethod + def SpiDev(spi_bus=0, spi_cs=0): + """ Init and return the SpiDev object + :return: SpiDev object + :param spi_bus: The RPi SPI bus to use: 0 or 1 + :param spi_cs: The RPi SPI chip select to use: 0 or 1 + :rtype: SpiDev + """ + BOARD.spi = spidev.SpiDev() + BOARD.spi.open(spi_bus, spi_cs) + BOARD.spi.max_speed_hz = 5000000 # SX127x can go up to 10MHz, pick half that to be safe + return BOARD.spi + + @staticmethod + def add_event_detect(dio_number, callback): + """ Wraps around the GPIO.add_event_detect function + :param dio_number: DIO pin 0...5 + :param callback: The function to call when the DIO triggers an IRQ. + :return: None + """ + GPIO.add_event_detect(dio_number, GPIO.RISING, callback=callback) + + @staticmethod + def add_events(cb_dio0, cb_dio1, cb_dio2, cb_dio3, cb_dio4, cb_dio5, switch_cb=None): + BOARD.add_event_detect(BOARD.DIO0, callback=cb_dio0) + BOARD.add_event_detect(BOARD.DIO1, callback=cb_dio1) + BOARD.add_event_detect(BOARD.DIO2, callback=cb_dio2) + BOARD.add_event_detect(BOARD.DIO3, callback=cb_dio3) + # the modtronix inAir9B does not expose DIO4 and DIO5 + if switch_cb is not None: + GPIO.add_event_detect(BOARD.SWITCH, GPIO.RISING, callback=switch_cb, bouncetime=300) + + @staticmethod + def led_on(value=1): + """ Switch the proto shields LED + :param value: 0/1 for off/on. Default is 1. + :return: value + :rtype : int + """ + GPIO.output(BOARD.LED, value) + return value + + @staticmethod + def led_off(): + """ Switch LED off + :return: 0 + """ + GPIO.output(BOARD.LED, 0) + return 0 + + @staticmethod + def blink(time_sec, n_blink): + if n_blink == 0: + return + BOARD.led_on() + for i in range(n_blink): + time.sleep(time_sec) + BOARD.led_off() + time.sleep(time_sec) + BOARD.led_on() + BOARD.led_off() diff --git a/pySX127x/SX127x/constants.py b/pySX127x/SX127x/constants.py new file mode 100755 index 0000000..02d4119 --- /dev/null +++ b/pySX127x/SX127x/constants.py @@ -0,0 +1,190 @@ +""" Defines constants (modes, bandwidths, registers, etc.) needed by SX127x. """ +# -*- coding: utf-8 -*- + +# Copyright 2015-2018 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +def add_lookup(cls): + """ A decorator that adds a lookup dictionary to the class. + The lookup dictionary maps the codes back to the names. This is used for pretty-printing. """ + varnames = filter(str.isupper, cls.__dict__.keys()) + lookup = dict(map(lambda varname: (cls.__dict__.get(varname, None), varname), varnames)) + setattr(cls, 'lookup', lookup) + return cls + + +@add_lookup +class MODE: + SLEEP = 0x80 + STDBY = 0x81 + FSTX = 0x82 + TX = 0x83 + FSRX = 0x84 + RXCONT = 0x85 + RXSINGLE = 0x86 + CAD = 0x87 + FSK_STDBY= 0x01 # needed for calibration + + +@add_lookup +class BW: + BW7_8 = 0 + BW10_4 = 1 + BW15_6 = 2 + BW20_8 = 3 + BW31_25 = 4 + BW41_7 = 5 + BW62_5 = 6 + BW125 = 7 + BW250 = 8 + BW500 = 9 + + +@add_lookup +class CODING_RATE: + CR4_5 = 1 + CR4_6 = 2 + CR4_7 = 3 + CR4_8 = 4 + + +@add_lookup +class GAIN: + NOT_USED = 0b000 + G1 = 0b001 + G2 = 0b010 + G3 = 0b011 + G4 = 0b100 + G5 = 0b101 + G6 = 0b110 + + +@add_lookup +class PA_SELECT: + RFO = 0 + PA_BOOST = 1 + + +@add_lookup +class PA_RAMP: + RAMP_3_4_ms = 0 + RAMP_2_ms = 1 + RAMP_1_ms = 2 + RAMP_500_us = 3 + RAMP_250_us = 4 + RAMP_125_us = 5 + RAMP_100_us = 6 + RAMP_62_us = 7 + RAMP_50_us = 8 + RAMP_40_us = 9 + RAMP_31_us = 10 + RAMP_25_us = 11 + RAMP_20_us = 12 + RAMP_15_us = 13 + RAMP_12_us = 14 + RAMP_10_us = 15 + + +class MASK: + class IRQ_FLAGS: + RxTimeout = 7 + RxDone = 6 + PayloadCrcError = 5 + ValidHeader = 4 + TxDone = 3 + CadDone = 2 + FhssChangeChannel = 1 + CadDetected = 0 + + +class REG: + + @add_lookup + class LORA: + FIFO = 0x00 + OP_MODE = 0x01 + FR_MSB = 0x06 + FR_MID = 0x07 + FR_LSB = 0x08 + PA_CONFIG = 0x09 + PA_RAMP = 0x0A + OCP = 0x0B + LNA = 0x0C + FIFO_ADDR_PTR = 0x0D + FIFO_TX_BASE_ADDR = 0x0E + FIFO_RX_BASE_ADDR = 0x0F + FIFO_RX_CURR_ADDR = 0x10 + IRQ_FLAGS_MASK = 0x11 + IRQ_FLAGS = 0x12 + RX_NB_BYTES = 0x13 + RX_HEADER_CNT_MSB = 0x14 + RX_PACKET_CNT_MSB = 0x16 + MODEM_STAT = 0x18 + PKT_SNR_VALUE = 0x19 + PKT_RSSI_VALUE = 0x1A + RSSI_VALUE = 0x1B + HOP_CHANNEL = 0x1C + MODEM_CONFIG_1 = 0x1D + MODEM_CONFIG_2 = 0x1E + SYMB_TIMEOUT_LSB = 0x1F + PREAMBLE_MSB = 0x20 + PAYLOAD_LENGTH = 0x22 + MAX_PAYLOAD_LENGTH = 0x23 + HOP_PERIOD = 0x24 + FIFO_RX_BYTE_ADDR = 0x25 + MODEM_CONFIG_3 = 0x26 + PPM_CORRECTION = 0x27 + FEI_MSB = 0x28 + DETECT_OPTIMIZE = 0X31 + INVERT_IQ = 0x33 + DETECTION_THRESH = 0X37 + SYNC_WORD = 0X39 + DIO_MAPPING_1 = 0x40 + DIO_MAPPING_2 = 0x41 + VERSION = 0x42 + TCXO = 0x4B + PA_DAC = 0x4D + AGC_REF = 0x61 + AGC_THRESH_1 = 0x62 + AGC_THRESH_2 = 0x63 + AGC_THRESH_3 = 0x64 + PLL = 0x70 + + @add_lookup + class FSK: + LNA = 0x0C + RX_CONFIG = 0x0D + RSSI_CONFIG = 0x0E + PREAMBLE_DETECT = 0x1F + OSC = 0x24 + SYNC_CONFIG = 0x27 + SYNC_VALUE_1 = 0x28 + SYNC_VALUE_2 = 0x29 + SYNC_VALUE_3 = 0x2A + SYNC_VALUE_4 = 0x2B + SYNC_VALUE_5 = 0x2C + SYNC_VALUE_6 = 0x2D + SYNC_VALUE_7 = 0x2E + SYNC_VALUE_8 = 0x2F + PACKET_CONFIG_1 = 0x30 + FIFO_THRESH = 0x35 + IMAGE_CAL = 0x3B + DIO_MAPPING_1 = 0x40 + DIO_MAPPING_2 = 0x41 diff --git a/pySX127x/VERSION b/pySX127x/VERSION new file mode 100755 index 0000000..ceab6e1 --- /dev/null +++ b/pySX127x/VERSION @@ -0,0 +1 @@ +0.1 \ No newline at end of file diff --git a/pySX127x/lora_util.py b/pySX127x/lora_util.py new file mode 100755 index 0000000..d3a7fa1 --- /dev/null +++ b/pySX127x/lora_util.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python2.7 + +""" This is a utility script for the SX127x (LoRa mode). It dumps all registers. """ + +# Copyright 2015 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +from SX127x.LoRa import * +from SX127x.board_config import BOARD +import argparse + +BOARD.setup() + +parser = argparse.ArgumentParser(description='LoRa utility functions') +parser.add_argument('--dump', '-d', dest='dump', default=False, action="store_true", help="dump all registers") +args = parser.parse_args() + +lora = LoRa(verbose=False) + +if args.dump: + + print("LoRa register dump:\n") + print("%02s %18s %2s %8s" % ('i', 'reg_name', 'v', 'v')) + print("-- ------------------ -- --------") + for reg_i, reg_name, val in lora.dump_registers(): + print("%02X %18s %02X %s" % (reg_i, reg_name, val, format(val, '#010b')[2:])) + print("") + +else: + print(lora) + +BOARD.teardown() \ No newline at end of file diff --git a/pySX127x/rx_cont.py b/pySX127x/rx_cont.py new file mode 100755 index 0000000..4e1c02b --- /dev/null +++ b/pySX127x/rx_cont.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +""" A simple continuous receiver class. """ + +# Copyright 2015 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +from time import sleep +from SX127x.LoRa import * +from SX127x.LoRaArgumentParser import LoRaArgumentParser +from SX127x.board_config import BOARD + +BOARD.setup() + +parser = LoRaArgumentParser("Continous LoRa receiver.") + + +class LoRaRcvCont(LoRa): + def __init__(self, verbose=False): + super(LoRaRcvCont, self).__init__(verbose) + self.set_mode(MODE.SLEEP) + self.set_dio_mapping([0] * 6) + + def on_rx_done(self): + BOARD.led_on() + print("\nRxDone") + self.clear_irq_flags(RxDone=1) + payload = self.read_payload(nocheck=True) + print(bytes(payload).decode()) + self.set_mode(MODE.SLEEP) + self.reset_ptr_rx() + BOARD.led_off() + self.set_mode(MODE.RXCONT) + + def on_tx_done(self): + print("\nTxDone") + print(self.get_irq_flags()) + + def on_cad_done(self): + print("\non_CadDone") + print(self.get_irq_flags()) + + def on_rx_timeout(self): + print("\non_RxTimeout") + print(self.get_irq_flags()) + + def on_valid_header(self): + print("\non_ValidHeader") + print(self.get_irq_flags()) + + def on_payload_crc_error(self): + print("\non_PayloadCrcError") + print(self.get_irq_flags()) + + def on_fhss_change_channel(self): + print("\non_FhssChangeChannel") + print(self.get_irq_flags()) + + def start(self): + self.reset_ptr_rx() + self.set_mode(MODE.RXCONT) + while True: + sleep(.5) + rssi_value = self.get_rssi_value() + status = self.get_modem_status() + sys.stdout.flush() + sys.stdout.write("\r%d %d %d" % (rssi_value, status['rx_ongoing'], status['modem_clear'])) + + +lora = LoRaRcvCont(verbose=False) +args = parser.parse_args(lora) + +lora.set_mode(MODE.STDBY) +lora.set_pa_config(pa_select=1) +#lora.set_rx_crc(True) +#lora.set_coding_rate(CODING_RATE.CR4_6) +#lora.set_pa_config(max_power=0, output_power=0) +#lora.set_lna_gain(GAIN.G1) +#lora.set_implicit_header_mode(False) +#lora.set_low_data_rate_optim(True) +#lora.set_pa_ramp(PA_RAMP.RAMP_50_us) +#lora.set_agc_auto_on(True) + +print(lora) +assert(lora.get_agc_auto_on() == 1) + +try: input("Press enter to start...") +except: pass + +try: + lora.start() +except KeyboardInterrupt: + sys.stdout.flush() + print("") + sys.stderr.write("KeyboardInterrupt\n") +finally: + sys.stdout.flush() + print("") + lora.set_mode(MODE.SLEEP) + print(lora) + BOARD.teardown() diff --git a/pySX127x/socket_client.py b/pySX127x/socket_client.py new file mode 100755 index 0000000..8b2a2f1 --- /dev/null +++ b/pySX127x/socket_client.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +# used for testing socket_transceiver.py +# connects to socket and allows user to send ascii payload + +import socket + +def sock_client(): + host = '127.0.0.1' + port = 20000 + + sock = socket.socket() + sock.connect((host,port)) + + message = input('>> ') + + while message != 'quit': + sock.send(bytearray(message,'utf-8')) + + data = bytearray(sock.recv(1024)).decode('ascii') + + print ('From LoRa: ' + data) + + message = input('>> ') + + sock.close() + +if __name__ == '__main__': + sock_client() diff --git a/pySX127x/socket_transceiver.py b/pySX127x/socket_transceiver.py new file mode 100755 index 0000000..f8a911f --- /dev/null +++ b/pySX127x/socket_transceiver.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +""" An asynchronous socket <-> LoRa interface """ + +# MIT License +# +# Copyright (c) 2016 bjcarne +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import sys, asyncore +from time import time +from SX127x.LoRa import * +from SX127x.board_config import BOARD + +BOARD.setup() + +class Server(asyncore.dispatcher): + def __init__(self, host, port): + asyncore.dispatcher.__init__(self) + self.create_socket() + self.set_reuse_addr() + self.bind((host, port)) + self.listen(1) + + def handle_accepted(self, sock, addr): + print("Connection from %s:%s" % sock.getpeername()) + self.conn = Handler(sock) + +class Handler(asyncore.dispatcher): + def __init__(self, sock): + asyncore.dispatcher.__init__(self, sock) + self.databuffer = b"" + self.tx_wait = 0 + + # when data is available on socket send to LoRa + def handle_read(self): + if not self.tx_wait: + data = self.recv(127) + print('Send:' + str(data)) + lora.write_payload(list(data)) + lora.set_dio_mapping([1,0,0,0,0,0]) # set DIO0 for txdone + lora.set_mode(MODE.TX) + self.tx_wait = 1 + + # when data for the socket, send + def handle_write(self): + if self.databuffer: + self.send(self.databuffer) + self.databuffer = b"" + + def handle_close(self): + print("Client disconnected") + self.close() + +class LoRaSocket(LoRa): + + def __init__(self, verbose=False): + super(LoRaSocket, self).__init__(verbose) + self.set_mode(MODE.SLEEP) + self.set_pa_config(pa_select=1) + self.set_max_payload_length(128) # set max payload to max fifo buffer length + self.payload = [] + self.set_dio_mapping([0] * 6) #initialise DIO0 for rxdone + + # when LoRa receives data send to socket conn + def on_rx_done(self): + payload = self.read_payload(nocheck=True) + + if len(payload) == 127: + self.payload[len(self.payload):] = payload + else: + self.payload[len(self.payload):] = payload + print('Recv:' + str(bytes(self.payload))) + + server.conn.databuffer = bytes(self.payload) #send to socket conn + self.payload = [] + + self.clear_irq_flags(RxDone=1) # clear rxdone IRQ flag + self.reset_ptr_rx() + self.set_mode(MODE.RXCONT) + + # after data sent by LoRa reset to receive mode + def on_tx_done(self): + self.clear_irq_flags(TxDone=1) # clear txdone IRQ flag + self.set_dio_mapping([0] * 6) + + self.set_mode(MODE.RXCONT) + server.conn.tx_wait = 0 + + +if __name__ == '__main__': + + server = Server('localhost', 20000) + + lora = LoRaSocket(verbose=False) + + print(lora) + + try: + asyncore.loop() + + except KeyboardInterrupt: + sys.stderr.write("\nKeyboardInterrupt\n") + + finally: + lora.set_mode(MODE.SLEEP) + print("Closing socket connection") + server.close() + BOARD.teardown() diff --git a/pySX127x/test_lora.py b/pySX127x/test_lora.py new file mode 100755 index 0000000..dbb673c --- /dev/null +++ b/pySX127x/test_lora.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python2.7 + +""" This script runs a small number of unit tests. """ + +# Copyright 2015 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +from SX127x.LoRa import * +from SX127x.board_config import BOARD +import unittest + + +def get_reg(reg_addr): + return lora.get_register(reg_addr) + + +def SaveState(reg_addr, n_registers=1): + """ This decorator wraps a get/set_register around the function (unittest) call. + :param reg_addr: Start of register addresses + :param n_registers: Number of registers to save. (Useful for MSB/LSB register pairs, etc.) + :return: + """ + def decorator(func): + def wrapper(self): + reg_bkup = lora.get_register(reg_addr) + func(self) + lora.set_register(reg_addr, reg_bkup) + return wrapper + return decorator + + +class TestSX127x(unittest.TestCase): + + def test_setter_getter(self): + bkup = lora.get_payload_length() + for l in [1,50, 128, bkup]: + lora.set_payload_length(l) + self.assertEqual(lora.get_payload_length(), l) + + @SaveState(REG.LORA.OP_MODE) + def test_mode(self): + mode = lora.get_mode() + for m in [MODE.STDBY, MODE.SLEEP, mode]: + lora.set_mode(m) + self.assertEqual(lora.get_mode(), m) + + @SaveState(REG.LORA.FR_MSB, n_registers=3) + def test_set_freq(self): + freq = lora.get_freq() + for f in [433.5, 434.5, 434.0, freq]: + lora.set_freq(f) + self.assertEqual(lora.get_freq(), f) + + @SaveState(REG.LORA.MODEM_CONFIG_3) + def test_set_agc_on(self): + lora.set_agc_auto_on(True) + self.assertEqual((get_reg(REG.LORA.MODEM_CONFIG_3) & 0b100) >> 2, 1) + lora.set_agc_auto_on(False) + self.assertEqual((get_reg(REG.LORA.MODEM_CONFIG_3) & 0b100) >> 2, 0) + + @SaveState(REG.LORA.MODEM_CONFIG_3) + def test_set_low_data_rate_optim(self): + lora.set_low_data_rate_optim(True) + self.assertEqual((get_reg(REG.LORA.MODEM_CONFIG_3) & 0b1000) >> 3, 1) + lora.set_low_data_rate_optim(False) + self.assertEqual((get_reg(REG.LORA.MODEM_CONFIG_3) & 0b1000) >> 3, 0) + + @SaveState(REG.LORA.DIO_MAPPING_1, 2) + def test_set_dio_mapping(self): + + dio_mapping = [1] * 6 + lora.set_dio_mapping(dio_mapping) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_1), 0b01010101) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_2), 0b01010000) + self.assertEqual(lora.get_dio_mapping(), dio_mapping) + + dio_mapping = [2] * 6 + lora.set_dio_mapping(dio_mapping) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_1), 0b10101010) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_2), 0b10100000) + self.assertEqual(lora.get_dio_mapping(), dio_mapping) + + dio_mapping = [0] * 6 + lora.set_dio_mapping(dio_mapping) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_1), 0b00000000) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_2), 0b00000000) + self.assertEqual(lora.get_dio_mapping(), dio_mapping) + + dio_mapping = [0,1,2,0,1,2] + lora.set_dio_mapping(dio_mapping) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_1), 0b00011000) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_2), 0b01100000) + self.assertEqual(lora.get_dio_mapping(), dio_mapping) + + dio_mapping = [1,2,0,1,2,0] + lora.set_dio_mapping(dio_mapping) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_1), 0b01100001) + self.assertEqual(get_reg(REG.LORA.DIO_MAPPING_2), 0b10000000) + self.assertEqual(lora.get_dio_mapping(), dio_mapping) + +# def test_set_lna_gain(self): +# bkup_lna_gain = lora.get_lna()['lna_gain'] +# for target_gain in [GAIN.NOT_USED, GAIN.G1, GAIN.G2, GAIN.G6, GAIN.NOT_USED, bkup_lna_gain]: +# print(target_gain) +# lora.set_lna_gain(target_gain) +# actual_gain = lora.get_lna()['lna_gain'] +# self.assertEqual(GAIN.lookup[actual_gain], GAIN.lookup[target_gain]) + + +if __name__ == '__main__': + + BOARD.setup() + lora = LoRa(verbose=False) + unittest.main() + BOARD.teardown() diff --git a/pySX127x/tx_beacon.py b/pySX127x/tx_beacon.py new file mode 100755 index 0000000..49c0e80 --- /dev/null +++ b/pySX127x/tx_beacon.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +""" A simple beacon transmitter class to send a 1-byte message (0x0f) in regular time intervals. """ + +# Copyright 2015 Mayer Analytics Ltd. +# +# This file is part of pySX127x. +# +# pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You can be released from the requirements of the license by obtaining a commercial license. Such a license is +# mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your +# own applications, or shipping pySX127x with a closed source product. +# +# You should have received a copy of the GNU General Public License along with pySX127. If not, see +# . + + +import sys +from time import sleep +from SX127x.LoRa import * +from SX127x.LoRaArgumentParser import LoRaArgumentParser +from SX127x.board_config import BOARD + +BOARD.setup() + +parser = LoRaArgumentParser("A simple LoRa beacon") +parser.add_argument('--single', '-S', dest='single', default=False, action="store_true", help="Single transmission") +parser.add_argument('--wait', '-w', dest='wait', default=1, action="store", type=float, help="Waiting time between transmissions (default is 0s)") + + +class LoRaBeacon(LoRa): + + tx_counter = 0 + + def __init__(self, verbose=False): + super(LoRaBeacon, self).__init__(verbose) + self.set_mode(MODE.SLEEP) + self.set_dio_mapping([1,0,0,0,0,0]) + + def on_rx_done(self): + print("\nRxDone") + print(self.get_irq_flags()) + print(map(hex, self.read_payload(nocheck=True))) + self.set_mode(MODE.SLEEP) + self.reset_ptr_rx() + self.set_mode(MODE.RXCONT) + + def on_tx_done(self): + global args + self.set_mode(MODE.STDBY) + self.clear_irq_flags(TxDone=1) + sys.stdout.flush() + self.tx_counter += 1 + sys.stdout.write("\rtx #%d" % self.tx_counter) + if args.single: + print + sys.exit(0) + BOARD.led_off() + sleep(args.wait) + self.write_payload([0x0f]) + BOARD.led_on() + self.set_mode(MODE.TX) + + def on_cad_done(self): + print("\non_CadDone") + print(self.get_irq_flags()) + + def on_rx_timeout(self): + print("\non_RxTimeout") + print(self.get_irq_flags()) + + def on_valid_header(self): + print("\non_ValidHeader") + print(self.get_irq_flags()) + + def on_payload_crc_error(self): + print("\non_PayloadCrcError") + print(self.get_irq_flags()) + + def on_fhss_change_channel(self): + print("\non_FhssChangeChannel") + print(self.get_irq_flags()) + + def start(self): + global args + sys.stdout.write("\rstart") + self.tx_counter = 0 + BOARD.led_on() + self.write_payload([0x0f]) + self.set_mode(MODE.TX) + while True: + sleep(1) + +lora = LoRaBeacon(verbose=False) +args = parser.parse_args(lora) + +lora.set_pa_config(pa_select=1) +#lora.set_rx_crc(True) +#lora.set_agc_auto_on(True) +#lora.set_lna_gain(GAIN.NOT_USED) +#lora.set_coding_rate(CODING_RATE.CR4_6) +#lora.set_implicit_header_mode(False) +#lora.set_pa_config(max_power=0x04, output_power=0x0F) +#lora.set_pa_config(max_power=0x04, output_power=0b01000000) +#lora.set_low_data_rate_optim(True) +#lora.set_pa_ramp(PA_RAMP.RAMP_50_us) + + +print(lora) +#assert(lora.get_lna()['lna_gain'] == GAIN.NOT_USED) +assert(lora.get_agc_auto_on() == 1) + +print("Beacon config:") +print(" Wait %f s" % args.wait) +print(" Single tx = %s" % args.single) +print("") +try: input("Press enter to start...") +except: pass + +try: + lora.start() +except KeyboardInterrupt: + sys.stdout.flush() + print("") + sys.stderr.write("KeyboardInterrupt\n") +finally: + sys.stdout.flush() + print("") + lora.set_mode(MODE.SLEEP) + print(lora) + BOARD.teardown()