diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1700fdd8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +*~ +rtkrcv.nav +.vscode/* +/logs/* +settings.conf +/data/* +/log/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 53b0be31..52c168f7 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,261 @@ -# rtkbase +# RTKBase -Some bash scripts for a simple gnss base station +### An easy to use and easy to install web frontend with some bash scripts and services for a simple gnss base station. -## Installation: +## FrontEnd: +|status|settings|logs| -+ Connect your gnss receiver to raspberry pi/orange pi/.... with usb or uart, and check which com port it uses (ttyS1, ttyAMA0, something else...) +Frontend's features are: -+ Set your gnss receiver to output raw data. If you need to use U-center from another computer, you can use `socat`: ++ View the satellites signal levels ++ View the base location on a map ++ Start/stop various services (Sending data to a Ntrip caster, Rtcm server, Log raw data to files) ++ Edit the services settings ++ Download/delete raw data - ``$ sudo socat tcp-listen:128,reuseaddr /dev/ttyS1,b115200,raw,echo=0`` - - Change the ttyS1 and 115200 value if needed. Then you can use a network connection in U-center with the base station ip address and the port n°128. +## Base example: +status -+ clone [RTKlib](https://github.com/tomojitakasu/RTKLIB/tree/rtklib_2.4.3) ++ Enclosure: GentleBOX JE-200 ++ SBC: Orange Pi Zero (512MB) ++ Gnss Receiver: U-Blox F9P (from Drotek) ++ Power: Trendnet TPE-113GI POE injector + Trendnet POE TPE-104GS Extractor/Splitter + DC Barrel to Micro Usb adapter - ``` - $ cd ~ - $ git clone -b rtklib_2.4.3 --single-branch https://github.com/tomojitakasu/RTKLIB - ``` +## Ready to flash release: +If you use a Raspberry Pi, thanks to [jancelin](https://github.com/jancelin), you can download a ready to flash iso file [here](https://github.com/jancelin/pi-gen/releases). -+ compile and install str2str: +## Easy installation: - Edit the CTARGET line in makefile in RTKLIB/app/str2str/gcc - - ``` - $ cd RTKLIB/app/str2str/gcc - $ nano makefile - ``` - - For an Orange Pi Zero SBC, i use: - - ``CTARGET = -mcpu=cortex-a7 -mfpu=neon-vfpv4 -funsafe-math-optimizations`` - - Then you can compile and install str2str: - - ``` - $ make - $ sudo make install - ``` ++ Connect your gnss receiver to your raspberry pi/orange pi/.... -+ clone this repository: ++ Open a terminal and: - ``` + ```bash $ cd ~ - $ git clone https://github.com/Stefal/rtkbase.git + $ wget https://raw.githubusercontent.com/stefal/rtkbase/web_gui/tools/install.sh + $ chmod +x install.sh + $ sudo ./install.sh --all ``` -+ Edit settings.conf: ++ Go grab a coffee, it's gonna take a while. The script will install the needed softwares, and if you use a Usb-connected U-Blox ZED-F9P receiver, it'll be detected and set to work as a base station. If you don't use a F9P, you will have to configure your receiver manually (see step 7 in manual installation), and choose the correct port from the settings page. - ``` - $ cd rtkbase - $ nano settings.conf - ``` ++ Open a web browser to `http://ip_of_your_sbc` (the script will try to show you this ip address). Default password is `admin`. The settings page allow you to enter your own settings for the base coordinates, ntrip credentials and so on... - The main parameters you should edit are `com_port`, `position`, and the NTRIP section if you send the stream to a caster. + all settings -+ If the U-blox gnss receiver is sets to its default settings (Raw output is disabled) you can permanently configure the receiver with `ubxconfig.sh`. For the ZED-F9P use + If you don't already know your base precise coordinates, it's time to read one of theses tutorial: + - [rtklibexplorer - Post-processing RTK - for single and dual frequency receivers](https://rtklibexplorer.wordpress.com/2020/02/05/rtklib-tips-for-using-a-cors-station-as-base/) + - [rtklibexplorer - PPP - for dual frequency receivers](https://rtklibexplorer.wordpress.com/2017/11/23/ppp-solutions-with-the-swiftnav-piksi-multi/) + - [Centipede documentation (in french)](https://jancelin.github.io/docs-centipedeRTK/docs/base/positionnement.html) + +## Manual installation: +The `install.sh` script can be use without the `--all` option to split the installation process into several differents steps: +```bash + $ ./install.sh --help + ################################ + RTKBASE INSTALLATION HELP + ################################ + Bash scripts for install a simple gnss base station with a web frontend. + + + + * Before install, connect your gnss receiver to raspberry pi/orange pi/.... with usb or uart. + * Running install script with sudo + + sudo ./install.sh + + Options: + --all + Install all dependencies, Rtklib, last release of Rtkbase, gpsd, chrony, services, + crontab jobs, detect your GNSS receiver and configure it. + + --dependencies + Install all dependencies like git build-essential python3-pip ... + + --rtklib + Clone RTKlib 2.4.3 from github and compile it. + https://github.com/tomojitakasu/RTKLIB/tree/rtklib_2.4.3 + + --rtkbase-release + Get last release of RTKBASE: + https://github.com/Stefal/rtkbase/releases + + --rtkbase-repo + Clone RTKBASE from github: + https://github.com/Stefal/rtkbase/tree/web_gui + + --unit-files + Deploy services. + + --gpsd-chrony + Install gpsd and chrony to set date and time + from the gnss receiver. + + --detect-usb-gnss + Detect your GNSS receiver. + + --configure-gnss + Configure your GNSS receiver. + + --start-services + Start services (rtkbase_web, str2str_tcp, gpsd, chrony) ``` - $ ./ubxconfig.sh /dev/your_com_port receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt +So, if you really want it, let's go for a manual installation with some explanations: +1. Install dependencies with `sudo ./install.sh --dependencies`, or do it manually with: + ```bash + $ sudo apt update + $ sudo apt install -y git build-essential python3-pip python3-dev python3-setuptools python3-wheel libsystemd-dev bc dos2unix socat ``` - This script will send the settings only if the firmware is the same release on the receiver and in the file. If your receiver use a more recent firmware, you can add the `--force` settings on the command line. + +1. Install RTKLIB with `sudo ./install.sh --rtklib`, or: + + clone [RTKlib](https://github.com/tomojitakasu/RTKLIB/tree/rtklib_2.4.3) + + ```bash + $ cd ~ + $ git clone -b rtklib_2.4.3 https://github.com/tomojitakasu/RTKLIB/rtklib_2.4.3 + ``` + + + compile and install str2str: + + Optionnaly, you can edit the CTARGET line in makefile in RTKLIB/app/str2str/gcc + + ```bash + $ cd RTKLIB/app/str2str/gcc + $ nano makefile + ``` + + For an Orange Pi Zero SBC, i use: + + ``CTARGET = -mcpu=cortex-a7 -mfpu=neon-vfpv4 -funsafe-math-optimizations`` + + Then you can compile and install str2str: + + ```bash + $ make + $ sudo make install + ``` + + Compile/install `rtkrcv` and `convbin` the same way as `str2str`. + +1. Get latest rtkbase release `sudo ./install.sh --rtkbase-release`, or: + ```bash + $ wget https://github.com/stefal/rtkbase/releases/latest/download/rtkbase.tar.gz -O rtkbase.tar.gz + $ tar -xvf rtkbase.tar.gz + ``` - $ ./ubxconfig.sh /dev/your_com_port receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt --force + If you prefer, you can clone this repository to get the latest code. + +1. Install the rtkbase requirements: + ```bash + $ python3 -m pip install --upgrade pip setuptools wheel --extra-index-url https://www.piwheels.org/simple + $ python3 -m pip install -r rtkbase/web_app/requirements.txt --extra-index-url https://www.piwheels.org/simple + $ python3 -m pip install rtkbase/tools/pystemd-0.8.1590398158-cp37-cp37m-linux_armv7l.whl + +1. Install the systemd services with `sudo ./install.sh --unit-files`, or do it manually with: + + Edit them (`rtkbase/unit/`) to replace `{user}` with your username. + + If you log the raw data inside the base station, you may want to compress these data and delete the too old archives. `archive_and_clean.sh` will do it for you. The default settings compress the previous day data and delete all archives older than 90 days. To automate these 2 tasks, enable the `rtkbase_archive.timer`. The default value runs the script everyday at 04H00. + + Copy these services to `/etc/systemd/system/` then enable the web server, str2str_tcp and rtkbase_archive.timer: + ```bash + $ sudo systemctl daemon-reload + $ sudo systemctl enable rtkbase_web + $ sudo systemctl enable str2str_tcp + $ sudo systemctl enable rtkbase_archive.timer ``` +1. Install and configure chrony and gpsd with `sudo ./install.sh --gpsd-chrony`, or: + + Install chrony with `sudo apt install chrony` then add this parameter in the chrony conf file (/etc/chrony/chrony.conf): -+ Do a quick test with ``$ ./run_cast.sh in_serial out_tcp`` you should see some data like this: - ``` - 2019/10/09 15:42:53 [CW---] 14020 B 19776 bps (0) /dev/ttyS1 (1) waiting... - 2019/10/09 15:42:58 [CW---] 26244 B 19558 bps (0) /dev/ttyS1 (1) waiting... - 2019/10/09 15:43:03 [CW---] 37956 B 19289 bps (0) /dev/ttyS1 (1) waiting... - 2019/10/09 15:43:08 [CW---] 49684 B 19551 bps (0) /dev/ttyS1 (1) waiting... - 2019/10/09 15:43:13 [CW---] 61488 B 17232 bps (0) /dev/ttyS1 (1) waiting... - 2019/10/09 15:43:18 [CW---] 73076 B 17646 bps (0) /dev/ttyS1 (1) waiting... - ``` - Stop the stream with + ```refclock SHM 0 refid GPS precision 1e-1 offset 0.2 delay 0.2``` + Edit the chrony unit file. You should set `After=gpsd.service` + + Install a gpsd release >= 3.2 or it won't work with a F9P. Its conf file should contains: ``` - $ sudo killall str2str - ``` - -+ If everything is ok, you can copy the unit files for systemd with this script: + # Devices gpsd should collect to at boot time. + # They need to be read/writeable, either by user gpsd or the group dialout. + DEVICES="tcp://127.0.0.1:5015" + + # Other options you want to pass to gpsd + GPSD_OPTIONS="-n -b" ``` - $ sudo ./copy_unit.sh + Edit the gpsd unit file. You should have someting like this in the "[Unit]" section: + ``` + [Unit] + Description=GPS (Global Positioning System) Daemon + Requires=gpsd.socket + BindsTo=str2str_tcp.service + After=str2str_tcp.service + ``` + + Reload the services and enable them: + ```bash + $ sudo systemctl daemon-reload + $ sudo systemctl enable chrony + $ sudo systemctl enable gpsd ``` -+ Then you can enable these services to autostart during boot: +1. Connect your gnss receiver to raspberry pi/orange pi/.... with usb or uart, and check which com port it uses (ttyS1, ttyAMA0, something else...). If it's a U-Blox usb receiver, you can use `sudo ./install.sh --detect-usb-gnss`. Write down the result, you may need it later. - ``$ sudo systemctl enable str2str_tcp.service`` <-- mandatory - ``$ sudo systemctl enable str2str_file.service`` <-- log data locally - ``$ sudo systemctl enable str2str_ntrip.service`` <-- send ntrip data to a caster +1. If you didn't have already configure your gnss receiver you must set it to output raw data: -+ You can start the services right now (ntrip and/or file), str2str_tcp.service will autostart as it is a dependency : + If it's a U-Blox ZED-F9P (usb), you can use + ```bash + $ sudo ./install.sh -detect-usb-gnss --configure-gnss + ``` - ``$ sudo systemctl start str2str_file.service`` - ``$ sudo systemctl start str2str_ntrip.service`` - -+ If you use `str2str_file` to log the data inside the base station, you may want to compress these data and delete the too old archives. For these 2 tasks, you can use `archive_and_clean.sh`. The default settings compress the previous day data and delete all archives older than 30 days. Edit your crontab with ``$ crontab -e`` and add these lines: + If it's a U-Blox ZED-F9P (uart), you can use this command (change the ttyS1 and 115200 value if needed)): + ```bash + $ rtkbase/tools/set_zed-f9p.sh /dev/ttyS1 115200 rtkbase/receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt ``` + + If you need to use a config tool from another computer (like U-center), you can use `socat`: + + ```bash + $ sudo socat tcp-listen:128,reuseaddr /dev/ttyS1,b115200,raw,echo=0 + ``` + + Change the ttyS1 and 115200 value if needed. Then you can use a network connection in U-center with the base station ip address and the port n°128. + +1. If you log the raw data inside the base station, you may want to compress these data and delete the too old archives. `archive_and_clean.sh` will do it for you. The default settings compress the previous day data and delete all archives older than 90 days. To automate these 2 tasks, you can use `sudo ./install.sh --crontab` or: + + Edit your crontab with ``$ crontab -e`` and add these lines: + ```bash SHELL=/bin/bash 0 4 * * * /home/YOUR_USER_NAME/PATH_TO_RTKBASE/archive_and_clean.sh ``` Cron will run this script everyday at 4H00. + +1. Now you can start the services with `sudo ./install.sh --start-services`, or: + ```bash + $ sudo systemctl start rtkbase_web + $ sudo systemctl start str2str_tcp + $ sudo systemctl start gpsd + $ sudo systemctl start chrony + ``` + + Everything should be ready, now you can open a web browser to your base station ip address. + +## How it works: +RTKBase use several RTKLIB `str2str` instances started with `run_cast.sh` as systemd services. `run_cast.sh` gets its settings from `settings.conf` ++ `str2str_tcp.service` is the main instance. It is connected to the gnss receiver and broadcast the raw data on TCP for all the others services. ++ `str2str_ntrip.service` get the data from the main instance, convert the data to rtcm and stream them to a Ntrip caster. ++ `str2str_rtcm_svr.service` get the data from the main instance, convert the data to rtcm and stream them to clients ++ `str2str_file.service` get the data from the main instance, and log the data to files. + +internal + +The web gui is available when the `rtkbase_web` service is running. + + +## License: +RTKBase is licensed under AGPL 3 (see LICENSE file). + +RTKBase uses some parts of others software: ++ [RTKLIB](https://github.com/tomojitakasu/RTKLIB) (BSD-2-Clause) ++ [ReachView](https://github.com/emlid/ReachView) (GPL v3) ++ [Flask](https://palletsprojects.com/p/flask/) [Jinja](https://palletsprojects.com/p/jinja/) [Werkzeug](https://palletsprojects.com/p/werkzeug/) (BSD-3-Clause) ++ [Flask SocketIO](https://github.com/miguelgrinberg/Flask-SocketIO) (MIT) ++ [Bootstrap](https://getbootstrap.com/) [Bootstrap Flask](https://github.com/greyli/bootstrap-flask) [Bootstrap 4 Toggle](https://gitbrent.github.io/bootstrap4-toggle/) [Bootstrap Table](https://bootstrap-table.com/) (MIT) ++ [wtforms](https://github.com/wtforms/wtforms/) (BSD-3-Clause) [Flask WTF](https://github.com/lepture/flask-wtf) (BSD) ++ [pystemd](https://github.com/facebookincubator/pystemd) (L-GPL 2.1) ++ [gpsd](https://gitlab.com/gpsd/gpsd) (BSD-2-Clause) + +RTKBase uses [OpenStreetMap](https://www.openstreetmap.org) tiles, courtesy of [Empreinte digitale](https://cloud.empreintedigitale.fr). diff --git a/archive_and_clean.sh b/archive_and_clean.sh index 0e2b881a..e2235b67 100755 --- a/archive_and_clean.sh +++ b/archive_and_clean.sh @@ -7,8 +7,10 @@ source <( grep = ${BASEDIR}/settings.conf ) cd ${datadir} #archive and compress previous day's gnss data. -find . -maxdepth 1 -type f -mtime -1 -mmin +60 -name "*.ubx*" -exec tar -jcvf ${archive_name} --remove-files {} +; +#find . -maxdepth 1 -type f -mtime -1 -mmin +60 -name "*.ubx*" -exec tar -jcvf ${archive_name} --remove-files {} +; +find . -maxdepth 1 -type f -mtime -1 -mmin +60 -name "*.ubx*" -exec zip -m9 ${archive_name} {} +; #delete gnss data older than x days. -find . -maxdepth 1 -type f -name "*.tar.bz2" -mtime +${archive_rotate} -delete +#find . -maxdepth 1 -type f -name "*.tar.bz2" -mtime +${archive_rotate} -delete +find . -maxdepth 1 -type f -name "*.zip" -mtime +${archive_rotate} -delete diff --git a/copy_unit.sh b/copy_unit.sh index ff0c2a7d..f87c441c 100755 --- a/copy_unit.sh +++ b/copy_unit.sh @@ -5,7 +5,7 @@ BASEDIR=$(dirname "$0") -for file_path in ${BASEDIR}/unit/*.service +for file_path in ${BASEDIR}/unit/*.service ${BASEDIR}/unit/*.timer do file_name=$(basename ${file_path}) echo copying ${file_name} diff --git a/images/base_f9p.jpg b/images/base_f9p.jpg new file mode 100644 index 00000000..6a5c1da3 Binary files /dev/null and b/images/base_f9p.jpg differ diff --git a/images/internal.png b/images/internal.png new file mode 100644 index 00000000..549b0638 Binary files /dev/null and b/images/internal.png differ diff --git a/images/web_all_settings.png b/images/web_all_settings.png new file mode 100644 index 00000000..c4292623 Binary files /dev/null and b/images/web_all_settings.png differ diff --git a/images/web_global.png b/images/web_global.png new file mode 100644 index 00000000..fe77113e Binary files /dev/null and b/images/web_global.png differ diff --git a/images/web_logs.png b/images/web_logs.png new file mode 100644 index 00000000..92b712d8 Binary files /dev/null and b/images/web_logs.png differ diff --git a/images/web_settings.png b/images/web_settings.png new file mode 100644 index 00000000..d3abb6db Binary files /dev/null and b/images/web_settings.png differ diff --git a/images/web_status.png b/images/web_status.png new file mode 100644 index 00000000..3b33f494 Binary files /dev/null and b/images/web_status.png differ diff --git a/receiver_cfg/U-Blox_ZED-F9P_rtkbase.old b/receiver_cfg/U-Blox_ZED-F9P_rtkbase.old new file mode 100644 index 00000000..0c33548e --- /dev/null +++ b/receiver_cfg/U-Blox_ZED-F9P_rtkbase.old @@ -0,0 +1,78 @@ +MON-VER - 0A 04 DC 00 45 58 54 20 43 4F 52 45 20 31 2E 30 30 20 28 39 34 65 35 36 65 29 00 00 00 00 00 00 00 00 30 30 31 39 30 30 30 30 00 00 52 4F 4D 20 42 41 53 45 20 30 78 31 31 38 42 32 30 36 30 00 00 00 00 00 00 00 00 00 00 00 46 57 56 45 52 3D 48 50 47 20 31 2E 31 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 50 52 4F 54 56 45 52 3D 32 37 2E 31 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4D 4F 44 3D 5A 45 44 2D 46 39 50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 47 50 53 3B 47 4C 4F 3B 47 41 4C 3B 42 44 53 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 51 5A 53 53 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 21 08 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 0B 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 37 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 09 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 02 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 06 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 38 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 07 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 21 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0A 08 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 22 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 04 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 61 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 39 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 13 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 14 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 09 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 34 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 01 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 02 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 07 00 00 00 01 00 00 +CFG-MSG - 06 01 08 00 01 3C 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 35 00 00 00 01 00 00 +CFG-MSG - 06 01 08 00 01 43 00 00 00 01 00 00 +CFG-MSG - 06 01 08 00 01 03 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 3B 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 24 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 25 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 23 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 20 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 26 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 21 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 11 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 01 12 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 02 14 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 02 15 00 00 00 01 00 00 +CFG-MSG - 06 01 08 00 02 59 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 02 32 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 02 13 00 00 00 01 00 00 +CFG-MSG - 06 01 08 00 0D 03 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0D 01 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 0D 06 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F0 00 01 01 01 01 01 00 +CFG-MSG - 06 01 08 00 F0 01 01 01 01 01 01 00 +CFG-MSG - 06 01 08 00 F0 02 01 01 01 01 01 00 +CFG-MSG - 06 01 08 00 F0 03 01 01 01 01 01 00 +CFG-MSG - 06 01 08 00 F0 04 01 01 01 01 01 00 +CFG-MSG - 06 01 08 00 F0 05 01 01 01 01 01 00 +CFG-MSG - 06 01 08 00 F0 06 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F0 07 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F0 08 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F0 09 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F0 0A 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F0 0D 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F0 0F 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F1 00 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F1 03 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F1 04 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 05 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 4A 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 4D 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 54 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 57 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 5E 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 61 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 7C 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 7F 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 E6 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 FE 00 00 00 00 00 00 +CFG-MSG - 06 01 08 00 F5 FD 00 00 00 00 00 00 +CFG-PRT - 06 00 14 00 00 00 00 00 84 00 00 00 00 00 00 00 23 00 23 00 00 00 00 00 +CFG-PRT - 06 00 14 00 01 00 00 00 C0 08 00 00 00 C2 01 00 23 00 21 00 00 00 00 00 +CFG-PRT - 06 00 14 00 02 00 00 00 C0 08 00 00 00 96 00 00 20 00 20 00 00 00 00 00 +CFG-PRT - 06 00 14 00 03 00 00 00 00 00 00 00 00 00 00 00 23 00 21 00 00 00 00 00 +CFG-PRT - 06 00 14 00 04 00 00 00 00 32 00 00 00 00 00 00 23 00 23 00 00 00 00 00 +CFG-RATE - 06 08 06 00 E8 03 01 00 01 00 +CFG-USB - 06 1B 6C 00 46 15 A9 01 00 00 00 00 00 00 02 00 75 2D 62 6C 6F 78 20 41 47 20 2D 20 77 77 77 2E 75 2D 62 6C 6F 78 2E 63 6F 6D 00 00 00 00 00 00 75 2D 62 6C 6F 78 20 47 4E 53 53 20 72 65 63 65 69 76 65 72 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 diff --git a/receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt b/receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt index 0c33548e..261e9537 100644 --- a/receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt +++ b/receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt @@ -1,78 +1,12 @@ -MON-VER - 0A 04 DC 00 45 58 54 20 43 4F 52 45 20 31 2E 30 30 20 28 39 34 65 35 36 65 29 00 00 00 00 00 00 00 00 30 30 31 39 30 30 30 30 00 00 52 4F 4D 20 42 41 53 45 20 30 78 31 31 38 42 32 30 36 30 00 00 00 00 00 00 00 00 00 00 00 46 57 56 45 52 3D 48 50 47 20 31 2E 31 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 50 52 4F 54 56 45 52 3D 32 37 2E 31 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4D 4F 44 3D 5A 45 44 2D 46 39 50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 47 50 53 3B 47 4C 4F 3B 47 41 4C 3B 42 44 53 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 51 5A 53 53 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 21 08 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 0B 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 37 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 09 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 02 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 06 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 38 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 07 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 21 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0A 08 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 22 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 04 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 61 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 39 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 13 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 14 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 09 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 34 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 01 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 02 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 07 00 00 00 01 00 00 -CFG-MSG - 06 01 08 00 01 3C 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 35 00 00 00 01 00 00 -CFG-MSG - 06 01 08 00 01 43 00 00 00 01 00 00 -CFG-MSG - 06 01 08 00 01 03 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 3B 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 24 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 25 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 23 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 20 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 26 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 21 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 11 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 01 12 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 02 14 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 02 15 00 00 00 01 00 00 -CFG-MSG - 06 01 08 00 02 59 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 02 32 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 02 13 00 00 00 01 00 00 -CFG-MSG - 06 01 08 00 0D 03 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0D 01 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 0D 06 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F0 00 01 01 01 01 01 00 -CFG-MSG - 06 01 08 00 F0 01 01 01 01 01 01 00 -CFG-MSG - 06 01 08 00 F0 02 01 01 01 01 01 00 -CFG-MSG - 06 01 08 00 F0 03 01 01 01 01 01 00 -CFG-MSG - 06 01 08 00 F0 04 01 01 01 01 01 00 -CFG-MSG - 06 01 08 00 F0 05 01 01 01 01 01 00 -CFG-MSG - 06 01 08 00 F0 06 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F0 07 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F0 08 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F0 09 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F0 0A 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F0 0D 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F0 0F 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F1 00 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F1 03 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F1 04 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 05 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 4A 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 4D 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 54 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 57 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 5E 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 61 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 7C 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 7F 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 E6 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 FE 00 00 00 00 00 00 -CFG-MSG - 06 01 08 00 F5 FD 00 00 00 00 00 00 -CFG-PRT - 06 00 14 00 00 00 00 00 84 00 00 00 00 00 00 00 23 00 23 00 00 00 00 00 -CFG-PRT - 06 00 14 00 01 00 00 00 C0 08 00 00 00 C2 01 00 23 00 21 00 00 00 00 00 -CFG-PRT - 06 00 14 00 02 00 00 00 C0 08 00 00 00 96 00 00 20 00 20 00 00 00 00 00 -CFG-PRT - 06 00 14 00 03 00 00 00 00 00 00 00 00 00 00 00 23 00 21 00 00 00 00 00 -CFG-PRT - 06 00 14 00 04 00 00 00 00 32 00 00 00 00 00 00 23 00 23 00 00 00 00 00 -CFG-RATE - 06 08 06 00 E8 03 01 00 01 00 -CFG-USB - 06 1B 6C 00 46 15 A9 01 00 00 00 00 00 00 02 00 75 2D 62 6C 6F 78 20 41 47 20 2D 20 77 77 77 2E 75 2D 62 6C 6F 78 2E 63 6F 6D 00 00 00 00 00 00 75 2D 62 6C 6F 78 20 47 4E 53 53 20 72 65 63 65 69 76 65 72 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +CFG-USBOUTPROT-NMEA,0 +CFG-UART1OUTPROT-NMEA,0 +CFG-MSGOUT-UBX_RXM_RAWX_USB,1 +CFG-MSGOUT-UBX_RXM_RAWX_UART1,1 +CFG-MSGOUT-UBX_RXM_SFRBX_USB,1 +CFG-MSGOUT-UBX_RXM_SFRBX_UART1,1 +CFG-MSGOUT-UBX_NAV_PVT_USB,1 +CFG-MSGOUT-UBX_NAV_PVT_UART1,1 +CFG-MSGOUT-UBX_NAV_SAT_USB,1 +CFG-MSGOUT-UBX_NAV_SAT_UART1,1 +CFG-MSGOUT-UBX_NAV_SIG_USB,1 +CFG-MSGOUT-UBX_NAV_SIG_UART1,1 diff --git a/receiver_cfg/ubxconfig.sh b/receiver_cfg/test_ubx.sh similarity index 98% rename from receiver_cfg/ubxconfig.sh rename to receiver_cfg/test_ubx.sh index e5884a5f..54eabc3d 100755 --- a/receiver_cfg/ubxconfig.sh +++ b/receiver_cfg/test_ubx.sh @@ -74,6 +74,8 @@ ver_check () { dev_mon_ver=`dev_mon_ver` file_mon_ver=`file_mon_ver $1` +echo "Device version: " ${dev_mon_ver} +echo "File version: " ${file_mon_ver} if [[ ${dev_mon_ver} =~ .*${file_mon_ver}.* ]] then @@ -105,6 +107,7 @@ then else return 1 fi +return 1 } send_ubx () diff --git a/rtkbase_update.sh b/rtkbase_update.sh new file mode 100755 index 00000000..4a4184a3 --- /dev/null +++ b/rtkbase_update.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +### THIS SCRIPT SHOULD NOT BE RUN MANUALLY ### +source_directory=$1 +destination_directory=$2 +data_dir=$3 +old_version=$4 +standard_user=$5 + +echo "remove existing rtkbase.old directory" +rm -rf /var/tmp/rtkbase.old +mkdir /var/tmp/rtkbase.old + +echo "copy rtkbase to rtkbase.old except /data directory" +#'shopt -s extglob' is needed for using (!pattern) exclusion pattern +#from inside a script +shopt -s extglob +cp -r ${destination_directory}/!(${data_dir}) /var/tmp/rtkbase.old + +echo "copy new release to destination" +cp -rfp ${source_directory}/. ${destination_directory} + +echo "delete the line version= in settings.conf" +sed -i '/version=/d' ${destination_directory}/settings.conf + +#change rtkbase's content owner +chown -R ${standard_user}:${standard_user} ${destination_directory} + +#if a reboot is needed +#sudo reboot now + +echo "Restart web server" +sudo systemctl restart rtkbase_web.service + + diff --git a/run_cast.sh b/run_cast.sh index 1558b400..11071479 100755 --- a/run_cast.sh +++ b/run_cast.sh @@ -8,41 +8,45 @@ BASEDIR=$(dirname "$0") source <( grep = ${BASEDIR}/settings.conf ) #import settings -in_serial="serial://${serial_input}" +in_serial="serial://${com_port}:${com_port_settings}#${receiver_format}" in_tcp="tcpcli://127.0.0.1:${tcp_port}#${receiver_format}" out_caster="ntrips://:${svr_pwd}@${svr_addr}:${svr_port}/${mnt_name}#rtcm3 -msg ${rtcm_msg} -p ${position} -i ${receiver}" out_tcp="tcpsvr://:${tcp_port}" -out_file="file://${datadir}/${file_name}::T::S=${file_rotate_time} -f ${file_overlap_time}" +out_file="file://${datadir}/${file_name}.${receiver_format}::T::S=${file_rotate_time} -f ${file_overlap_time}" +out_rtcm_svr="tcpsvr://:${rtcm_svr_port}#rtcm3 -msg ${rtcm_svr_msg} -p ${position}" - -# start NTRIP caster - - - mkdir -p ${logdir} ${datadir} +mkdir -p ${logdir} + + case "$2" in + out_tcp) + #echo ${cast} -in ${!1} -out $out_tcp + # What is this ${!1} ? It's variable indirection + ${cast} -in ${!1} -out ${out_tcp} -t ${level} -fl ${logdir}/str2str_tcp.log & + ;; + + out_caster) + #echo ${cast} -in ${!1} -out $out_caster + ${cast} -in ${!1} -out ${out_caster} -t ${level} -fl ${logdir}/str2str_ntrip.log & + ;; + + out_rtcm_svr) + #echo ${cast} -in ${!1} -out $out_rtcm_svr + ${cast} -in ${!1} -out ${out_rtcm_svr} -t ${level} -fl ${logdir}/str2str_rtcm_svr.log & + ;; + + out_file) + #echo ${cast} -in ${!1} -out $out_caster + ${BASEDIR}/check_timesync.sh #wait for a correct date/time before starting to write files + ret=$? + if [ ${ret} -eq 0 ] + then + mkdir -p ${datadir} + ${cast} -in ${!1} -out ${out_file} -t ${level} -fl ${logdir}/str2str_file.log & + fi + ;; - case "$2" in - out_tcp) - #echo ${cast} -in ${!1} -out $out_tcp - ${cast} -in ${!1} -out ${out_tcp} & - ;; - - out_caster) - #echo ${cast} -in ${!1} -out $out_caster - ${cast} -in ${!1} -out ${out_caster} & - ;; - - out_file) - #echo ${cast} -in ${!1} -out $out_caster - ${BASEDIR}/check_timesync.sh #wait for a correct date/time before starting to write files - ret=$? - if [ ${ret} -eq 0 ] - then - ${cast} -in ${!1} -out ${out_file} & - fi - ;; - - esac + esac diff --git a/settings.conf b/settings.conf deleted file mode 100644 index 5f3ea5e1..00000000 --- a/settings.conf +++ /dev/null @@ -1,38 +0,0 @@ -[general] -basedir=$(dirname "$0") -cast=/usr/local/bin/str2str -web_password= -web_password_hash=pbkdf2:sha256:150000$kWdEE8eU$d30b1a75e5cf898684bad60b47a45a8058b6c33535560be005b6e0110b947cf6 -web_authentification=true - -[main] -position='47.034 -1.251 36.4' -com_port='ttyS1' -com_port_settings='115200:8:n:1' -receiver=Ublox_neo-m8t -receiver_format='ubx' -serial_input="${com_port}:${com_port_settings}#${receiver_format}" -tcp_port=5015 - -[local_storage] -datadir=$BASEDIR/data -file_name="%Y-%m-%d-%h:%M:%S-GNSS-1.ubx" -file_rotate_time=24 -file_overlap_time=30 -archive_name=$(date -d "-1 days" +"%Y-%m-%d_%S").tar.bz2 -archive_rotate=30 - -[ntrip] -svr_addr=caster.centipede.fr -svr_port=2101 -svr_pwd=none -mnt_name=Your_mount_name -rtcm_msg='1004,1005,1006,1012,1019,1020,1042,1045,1046,1077,1087,1097,1107,1127' - -[log] -logdir=$BASEDIR/log -level=0 -log1=$logdir/cast_trac_`date -u +%Y%m%d_%H%M%S`.log -log2=$logdir/cast_stat_`date -u +%Y%m%d_%H%M%S`.log -log3=$logdir/ifconfig_`date -u +%Y%m%d_%H%M%S`.log - diff --git a/settings.conf.default b/settings.conf.default new file mode 100644 index 00000000..e25e8c44 --- /dev/null +++ b/settings.conf.default @@ -0,0 +1,83 @@ +# settings for run_cast.sh + +[general] +# Version +version=2.0.0-beta.36 +# Rtkbase upgrade mandatory "checkpoint" +checkpoint_version=2.1.0 +# User who runs str2str_file service +user= +# NTRIP caster program +BASEDIR=$(dirname "$0") +#path to the caster application +cast=/usr/local/bin/str2str +#set true if you want to enter a password to open the web gui. +web_authentification=true +#enter here your new password for web gui +#(it will be erased during the next web server start) +new_web_password= +#don't touch this one +web_password_hash=pbkdf2:sha256:150000$kWdEE8eU$d30b1a75e5cf898684bad60b47a45a8058b6c33535560be005b6e0110b947cf6 + +[main] +#base coordinates: lat long height +position='47.034 -1.251 36.4' +#gnss receiver com port +com_port='ttyS1' +#gnss receiver com port settings +com_port_settings='115200:8:n:1' +#receiver model +receiver='Ublox_ZED-F9P' +#gnss receiver format +receiver_format='ubx' +#tcp port for RAW stream. If you change this value, edit gpsd conf file to reflect the port in DEVICES. +tcp_port='5015' + +[local_storage] + +# File options for local data storage + +#gnss data storage directory +datadir=$BASEDIR/data +#gnss data filename +file_name='%Y-%m-%d_%h-%M-%S_GNSS-1' +#file rotate time in hour +file_rotate_time='24' +#file overlap time in seconds +file_overlap_time='30' +#name for the compressed archive +archive_name=$(date -d "-1 days" +"%Y-%m-%d_%S").zip +#archives older than this value (in days) will be deleted by archive_and_clean.sh +archive_rotate='90' + +[ntrip] + +# NTRIP caster options + +#ntrip caster url +svr_addr='caster.centipede.fr' +#ntrip caster port +svr_port='2101' +#ntrip caster password +svr_pwd='' +#Mount name +mnt_name='Your_mount_name' + +rtcm_msg='1004,1005,1006,1012,1019,1020,1042,1045,1046,1077,1087,1097,1107,1127,1230' + +[rtcm_svr] + +# RTCM server options + +#port for rtcm local use +rtcm_svr_port='5016' +#messages for rtcm local use +rtcm_svr_msg='1004,1005,1006,1012,1019,1020,1042,1045,1046,1077,1087,1097,1107,1127,1230' + + +[log] + +#log directory +logdir=$BASEDIR/logs +#log trace level (0: no trace) +level=0 diff --git a/tools/detect_usb_receiver.sh b/tools/detect_usb_receiver.sh new file mode 100755 index 00000000..f0da0118 --- /dev/null +++ b/tools/detect_usb_receiver.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# source https://unix.stackexchange.com/a/144735 +for sysdevpath in $(find /sys/bus/usb/devices/usb*/ -name dev); do + + syspath="${sysdevpath%/dev}" + devname="$(udevadm info -q name -p $syspath)" + if [[ "$devname" == "bus/"* ]]; then continue; fi + eval "$(udevadm info -q property --export -p $syspath)" + if [[ -z "$ID_SERIAL" ]]; then continue; fi + echo "/dev/$devname - $ID_SERIAL" + +done diff --git a/tools/gps/__init__.py b/tools/gps/__init__.py new file mode 100755 index 00000000..0ef02a22 --- /dev/null +++ b/tools/gps/__init__.py @@ -0,0 +1,26 @@ +# Make core client functions available without prefix. +# This code is generated by scons. Do not hand-hack it! +# +# This file is Copyright 2010 by the GPSD project +# SPDX-License-Identifier: BSD-2-Clause +# +# This code runs compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +from __future__ import absolute_import # Ensure Python2 behaves like Python 3 + +from .gps import * +from .misc import * + +# Keep in sync with gpsd.h +api_version_major = 3 # bumped on incompatible changes +api_version_minor = 14 # bumped on compatible changes + +# at some point this will need an override method +__iconpath__ = '//usr/local/share/gpsd/icons' + +__version__ = '3.20.1~dev' + +# The 'client' module exposes some C utility functions for Python clients. +# The 'packet' module exposes the packet getter via a Python interface. + +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/aiogps.py b/tools/gps/aiogps.py new file mode 100644 index 00000000..81d663cd --- /dev/null +++ b/tools/gps/aiogps.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright 2019 Grand Joldes (grandwork2@yahoo.com). +# +# This file is Copyright 2019 by the GPSD project +# SPDX-License-Identifier: BSD-2-clause + +# This code run compatibly under Python 3.x for x >= 6. + +"""aiogps.py -- Asyncio Python interface to GPSD. + +This module adds asyncio support to the Python gps interface. It runs on +Python versions >= 3.6 and provides the following benefits: + - easy integration in asyncio applications (all I/O operations done through + non-blocking coroutines, async context manager, async iterator); + - support for cancellation (all operations are cancellable); + - support for timeouts (on both read and connect); + - support for connection keep-alive (using the TCP keep alive mechanism) + - support for automatic re-connection; + - configurable connection parameters; + - configurable exeption handling (internally or by application); + - logging support (logger name: 'gps.aiogps'). + +The use of timeouts, keepalive and automatic reconnection make possible easy +handling of GPSD connections over unreliable networks. + +Examples: + import logging + import gps.aiogps + + # configuring logging + logging.basicConfig() + logging.root.setLevel(logging.INFO) + # Example of setting up logging level for the aiogps logger + logging.getLogger('gps.aiogps').setLevel(logging.ERROR) + + # using default parameters + async with gps.aiogps.aiogps() as gpsd: + async for msg in gpsd: + # Log last message + logging.info(f'Received: {msg}') + # Log updated GPS status + logging.info(f'\nGPS status:\n{gpsd}') + + # using custom parameters + try: + async with gps.aiogps.aiogps( + connection_args = { + 'host': '192.168.10.116', + 'port': 2947 + }, + connection_timeout = 5, + reconnect = 0, # do not try to reconnect, raise exceptions + alive_opts = { + 'rx_timeout': 5 + } + ) as gpsd: + async for msg in gpsd: + logging.info(msg) + except asyncio.CancelledError: + return + except asyncio.IncompleteReadError: + logging.info('Connection closed by server') + except asyncio.TimeoutError: + logging.error('Timeout waiting for gpsd to respond') + except Exception as exc: + logging.error(f'Error: {exc}') + +""" + +__all__ = ['aiogps', ] + +import logging +import asyncio +import socket +from typing import Optional, Union, Awaitable + +from .client import gpsjson, dictwrapper +from .gps import gps, gpsdata, WATCH_ENABLE, PACKET_SET +from .misc import polystr, polybytes + + +class aiogps(gps): # pylint: disable=R0902 + """An asyncio gps client. + + Reimplements all gps IO methods using asyncio coros. Adds connection + management, an asyncio context manager and an asyncio iterator. + + The class uses a logger named 'gps.aiogps' to record events. The logger is + configured with a NullHandler to disable any message logging until the + application configures another handler. + """ + + def __init__(self, # pylint: disable=W0231 + connection_args: Optional[dict] = None, + connection_timeout: Optional[float] = None, + reconnect: Optional[float] = 2, + alive_opts: Optional[dict] = None) -> None: + """ + Arguments: + connection_args: arguments needed for opening a connection. + These will be passed directly to asyncio.open_connection. + If set to None, a connection to the default gps host and port + will be attempded. + connection_timeout: time to wait for a connection to complete + (seconds). Set to None to disable. + reconnect: configures automatic reconnections: + - 0: reconnection is not attempted in case of an error and the + error is raised to the user; + - number > 0: delay until next reconnection attempt (seconds). + alive_opts: options related to detection of disconnections. + Two mecanisms are supported: TCP keepalive (default, may not be + available on all platforms) and Rx timeout, through the + following options: + - rx_timeout: Rx timeout (seconds). Set to None to disable. + - SO_KEEPALIVE: socket keepalive and related parameters: + - TCP_KEEPIDLE + - TCP_KEEPINTVL + - TCP_KEEPCNT + """ + # If connection_args are not specified use defaults + self.connection_args = connection_args or { + 'host': self.host, + 'port': self.port + } + self.connection_timeout = connection_timeout + assert reconnect >= 0 + self.reconnect = reconnect + # If alive_opts are not specified use defaults + self.alive_opts = alive_opts or { + 'rx_timeout': None, + 'SO_KEEPALIVE': 1, + 'TCP_KEEPIDLE': 2, + 'TCP_KEEPINTVL': 2, + 'TCP_KEEPCNT': 3 + } + # Connection access streams + self.reader: Optional[asyncio.StreamReader] = None + self.writer: Optional[asyncio.StreamWriter] = None + # Set up logging + self.logger = logging.getLogger(__name__) + # Set the Null handler - prevents logging message handling unless the + # application sets up a handler. + self.logger.addHandler(logging.NullHandler()) + # Init gps parents + gpsdata.__init__(self) # pylint: disable=W0233 + gpsjson.__init__(self) # pylint: disable=W0233 + # Provide the response in both 'str' and 'bytes' form + self.bresponse = b'' + self.response = polystr(self.bresponse) + # Default stream command + self.stream_command = self.generate_stream_command(WATCH_ENABLE) + self.loop = self.connection_args.get('loop', asyncio.get_event_loop()) + + def __del__(self) -> None: + """ Destructor """ + self.close() + + async def _open_connection(self) -> None: + """ + Opens a connection to the GPSD server and configures the TCP socket. + """ + self.logger.info( + f"Connecting to gpsd at {self.connection_args['host']}" + + (f":{self.connection_args['port']}" + if self.connection_args['port'] else '')) + self.reader, self.writer = await asyncio.wait_for( + asyncio.open_connection(**self.connection_args), + self.connection_timeout, + loop=self.loop) + # Set socket options + sock = self.writer.get_extra_info('socket') + if sock is not None: + if 'SO_KEEPALIVE' in self.alive_opts: + sock.setsockopt(socket.SOL_SOCKET, + socket.SO_KEEPALIVE, + self.alive_opts['SO_KEEPALIVE']) + if hasattr( + sock, + 'TCP_KEEPIDLE') and 'TCP_KEEPIDLE' in self.alive_opts: + sock.setsockopt(socket.IPPROTO_TCP, + socket.TCP_KEEPIDLE, # pylint: disable=E1101 + self.alive_opts['TCP_KEEPIDLE']) + if hasattr( + sock, + 'TCP_KEEPINTVL') and 'TCP_KEEPINTVL' in self.alive_opts: + sock.setsockopt(socket.IPPROTO_TCP, + socket.TCP_KEEPINTVL, # pylint: disable=E1101 + self.alive_opts['TCP_KEEPINTVL']) + if hasattr( + sock, + 'TCP_KEEPCNT') and 'TCP_KEEPCNT' in self.alive_opts: + sock.setsockopt(socket.IPPROTO_TCP, + socket.TCP_KEEPCNT, + self.alive_opts['TCP_KEEPCNT']) + + def close(self) -> None: + """ Closes connection to GPSD server """ + if self.writer: + try: + self.writer.close() + except Exception: # pylint: disable=W0703 + pass + self.writer = None + + def waiting(self) -> bool: # pylint: disable=W0221 + """ Mask the blocking waiting method from gpscommon """ + return True + + async def read(self) -> Union[dictwrapper, str]: + """ Reads data from GPSD server """ + while True: + await self.connect() + try: + rx_timeout = self.alive_opts.get('rx_timeout', None) + reader = self.reader.readuntil(separator=b'\n') + self.bresponse = await asyncio.wait_for(reader, + rx_timeout, + loop=self.loop) + self.response = polystr(self.bresponse) + if self.response.startswith( + "{") and self.response.endswith("}\r\n"): + self.unpack(self.response) + self._oldstyle_shim() + self.valid |= PACKET_SET + return self.data + return self.response + except asyncio.CancelledError: + self.close() + raise + except Exception as exc: # pylint: disable=W0703 + error = 'timeout' if isinstance( + exc, asyncio.TimeoutError) else exc + self.logger.warning( + f'Failed to get message from GPSD: {error}') + self.close() + if self.reconnect: + # Try again later + await asyncio.sleep(self.reconnect) + else: + raise + + async def connect(self) -> None: # pylint: disable=W0221 + """ Connects to GPSD server and starts streaming data """ + while not self.writer: + try: + await self._open_connection() + await self.stream() + self.logger.info('Connected to gpsd') + except asyncio.CancelledError: + self.close() + raise + except Exception as exc: # pylint: disable=W0703 + error = 'timeout' if isinstance( + exc, asyncio.TimeoutError) else exc + self.logger.error(f'Failed to connect to GPSD: {error}') + self.close() + if self.reconnect: + # Try again later + await asyncio.sleep(self.reconnect) + else: + raise + + async def send(self, commands) -> None: + """ Sends commands """ + bcommands = polybytes(commands + "\n") + if self.writer: + self.writer.write(bcommands) + await self.writer.drain() + + async def stream(self, flags: Optional[int] = 0, + devpath: Optional[str] = None) -> None: + """ Creates and sends the stream command """ + if flags > 0: + # Update the stream command + self.stream_command = self.generate_stream_command(flags, devpath) + + if self.stream_command: + self.logger.info(f'Sent stream as: {self.stream_command}') + await self.send(self.stream_command) + else: + raise TypeError(f'Invalid streaming command: {flags}') + + async def __aenter__(self) -> 'aiogps': + """ Context manager entry """ + return self + + async def __aexit__(self, exc_type, exc, traceback) -> None: + """ Context manager exit: close connection """ + self.close() + + def __aiter__(self) -> 'aiogps': + """ Async iterator interface """ + return self + + async def __anext__(self) -> Union[dictwrapper, str]: + """ Returns next message from GPSD """ + data = await self.read() + return data + + def __next__(self) -> Awaitable: + """ + Reimplementation of the blocking iterator from gps. + Returns an awaitable which returns the next message from GPSD. + """ + return self.read() + +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/client.py b/tools/gps/client.py new file mode 100644 index 00000000..482ffe3d --- /dev/null +++ b/tools/gps/client.py @@ -0,0 +1,342 @@ +"gpsd client functions" +# This file is Copyright 2019 by the GPSD project +# SPDX-License-Identifier: BSD-2-Clause +# +# This code run compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +from __future__ import absolute_import, print_function, division + +import json +import select +import socket +import sys +import time + +from .misc import polystr, polybytes +from .watch_options import * + +GPSD_PORT = "2947" + + +class gpscommon(object): + "Isolate socket handling and buffering from the protocol interpretation." + + host = "127.0.0.1" + port = GPSD_PORT + + def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0, + should_reconnect=False): + self.stream_command = b'' + self.linebuffer = b'' + self.received = time.time() + self.reconnect = should_reconnect + self.verbose = verbose + self.sock = None # in case we blow up in connect + # Provide the response in both 'str' and 'bytes' form + self.bresponse = b'' + self.response = polystr(self.bresponse) + + if host is not None and port is not None: + self.host = host + self.port = port + self.connect(self.host, self.port) + + def connect(self, host, port): + """Connect to a host on a given port. + + If the hostname ends with a colon (`:') followed by a number, and + there is no port specified, that suffix will be stripped off and the + number interpreted as the port number to use. + """ + if not port and (host.find(':') == host.rfind(':')): + i = host.rfind(':') + if i >= 0: + host, port = host[:i], host[i + 1:] + try: + port = int(port) + except ValueError: + raise socket.error("nonnumeric port") + # if self.verbose > 0: + # print 'connect:', (host, port) + msg = "getaddrinfo returns an empty list" + self.sock = None + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, _canonname, sa = res + try: + self.sock = socket.socket(af, socktype, proto) + # if self.debuglevel > 0: print 'connect:', (host, port) + self.sock.connect(sa) + if self.verbose > 0: + print('connected to tcp://{}:{}'.format(host, port)) + break + # do not use except ConnectionRefusedError + # # Python 2.7 doc does have this exception + except socket.error as e: + if self.verbose > 1: + msg = str(e) + ' (to {}:{})'.format(host, port) + sys.stderr.write("error: {}\n".format(msg.strip())) + self.close() + raise # propogate error to caller + + def close(self): + "Close the gpsd socket" + if self.sock: + self.sock.close() + self.sock = None + + def __del__(self): + "Close the gpsd socket" + self.close() + + def waiting(self, timeout=0): + "Return True if data is ready for the client." + if self.linebuffer: + return True + if self.sock is None: + return False + + (winput, _woutput, _wexceptions) = select.select( + (self.sock,), (), (), timeout) + return winput != [] + + def read(self): + "Wait for and read data being streamed from the daemon." + + if None is self.sock: + self.connect(self.host, self.port) + if None is self.sock: + return -1 + self.stream() + + eol = self.linebuffer.find(b'\n') + if eol == -1: + # RTCM3 JSON can be over 4.4k long, so go big + frag = self.sock.recv(8192) + + self.linebuffer += frag + if not self.linebuffer: + if self.verbose > 1: + sys.stderr.write( + "poll: no available data: returning -1.\n") + # Read failed + return -1 + + eol = self.linebuffer.find(b'\n') + if eol == -1: + if self.verbose > 1: + sys.stderr.write("poll: partial message: returning 0.\n") + # Read succeeded, but only got a fragment + self.response = '' # Don't duplicate last response + self.bresponse = '' # Don't duplicate last response + return 0 + else: + if self.verbose > 1: + sys.stderr.write("poll: fetching from buffer.\n") + + # We got a line + eol += 1 + # Provide the response in both 'str' and 'bytes' form + self.bresponse = self.linebuffer[:eol] + self.response = polystr(self.bresponse) + self.linebuffer = self.linebuffer[eol:] + + # Can happen if daemon terminates while we're reading. + if not self.response: + return -1 + if 1 < self.verbose: + sys.stderr.write("poll: data is %s\n" % repr(self.response)) + self.received = time.time() + # We got a \n-terminated line + return len(self.response) + + # Note that the 'data' method is sometimes shadowed by a name + # collision, rendering it unusable. The documentation recommends + # accessing 'response' directly. Consequently, no accessor method + # for 'bresponse' is currently provided. + + def data(self): + "Return the client data buffer." + return self.response + + def send(self, commands): + "Ship commands to the daemon." + lineend = "\n" + if isinstance(commands, bytes): + lineend = polybytes("\n") + if not commands.endswith(lineend): + commands += lineend + + if self.sock is None: + self.stream_command = commands + else: + self.sock.send(polybytes(commands)) + + +class json_error(BaseException): + "Class for JSON errors" + + def __init__(self, data, explanation): + BaseException.__init__(self) + self.data = data + self.explanation = explanation + + +class gpsjson(object): + "Basic JSON decoding." + + def __init__(self): + self.data = None + self.stream_command = None + self.enqueued = None + self.verbose = -1 + + def __iter__(self): + "Broken __iter__" + return self + + def unpack(self, buf): + "Unpack a JSON string" + try: + self.data = dictwrapper(json.loads(buf.strip(), encoding="ascii")) + except ValueError as e: + raise json_error(buf, e.args[0]) + # Should be done for any other array-valued subobjects, too. + # This particular logic can fire on SKY or RTCM2 objects. + if hasattr(self.data, "satellites"): + self.data.satellites = [dictwrapper(x) + for x in self.data.satellites] + + def stream(self, flags=0, devpath=None): + "Control streaming reports from the daemon," + + if 0 < flags: + self.stream_command = self.generate_stream_command(flags, devpath) + else: + self.stream_command = self.enqueued + + if self.stream_command: + if self.verbose > 1: + sys.stderr.write("send: stream as:" + " {}\n".format(self.stream_command)) + self.send(self.stream_command) + else: + raise TypeError("Invalid streaming command!! : " + str(flags)) + + def generate_stream_command(self, flags=0, devpath=None): + "Generate stream command" + if flags & WATCH_OLDSTYLE: + return self.generate_stream_command_old_style(flags) + + return self.generate_stream_command_new_style(flags, devpath) + + @staticmethod + def generate_stream_command_old_style(flags=0): + "Generate stream command, old style" + if flags & WATCH_DISABLE: + arg = "w-" + if flags & WATCH_NMEA: + arg += 'r-' + + elif flags & WATCH_ENABLE: + arg = 'w+' + if flags & WATCH_NMEA: + arg += 'r+' + + return arg + + @staticmethod + def generate_stream_command_new_style(flags=0, devpath=None): + "Generate stream command, new style" + + if (flags & (WATCH_JSON | WATCH_OLDSTYLE | WATCH_NMEA | + WATCH_RAW)) == 0: + flags |= WATCH_JSON + + if flags & WATCH_DISABLE: + arg = '?WATCH={"enable":false' + if flags & WATCH_JSON: + arg += ',"json":false' + if flags & WATCH_NMEA: + arg += ',"nmea":false' + if flags & WATCH_RARE: + arg += ',"raw":1' + if flags & WATCH_RAW: + arg += ',"raw":2' + if flags & WATCH_SCALED: + arg += ',"scaled":false' + if flags & WATCH_TIMING: + arg += ',"timing":false' + if flags & WATCH_SPLIT24: + arg += ',"split24":false' + if flags & WATCH_PPS: + arg += ',"pps":false' + else: # flags & WATCH_ENABLE: + arg = '?WATCH={"enable":true' + if flags & WATCH_JSON: + arg += ',"json":true' + if flags & WATCH_NMEA: + arg += ',"nmea":true' + if flags & WATCH_RARE: + arg += ',"raw":1' + if flags & WATCH_RAW: + arg += ',"raw":2' + if flags & WATCH_SCALED: + arg += ',"scaled":true' + if flags & WATCH_TIMING: + arg += ',"timing":true' + if flags & WATCH_SPLIT24: + arg += ',"split24":true' + if flags & WATCH_PPS: + arg += ',"pps":true' + if flags & WATCH_DEVICE: + arg += ',"device":"%s"' % devpath + arg += "}" + return arg + + +class dictwrapper(object): + "Wrapper that yields both class and dictionary behavior," + + def __init__(self, ddict): + "Init class dictwrapper" + self.__dict__ = ddict + + def get(self, k, d=None): + "Get dictwrapper" + return self.__dict__.get(k, d) + + def keys(self): + "Keys dictwrapper" + return self.__dict__.keys() + + def __getitem__(self, key): + "Emulate dictionary, for new-style interface." + return self.__dict__[key] + + def __iter__(self): + "Iterate dictwrapper" + return self.__dict__.__iter__() + + def __setitem__(self, key, val): + "Emulate dictionary, for new-style interface." + self.__dict__[key] = val + + def __contains__(self, key): + "Find key in dictwrapper" + return key in self.__dict__ + + def __str__(self): + "dictwrapper to string" + return "" + __repr__ = __str__ + + def __len__(self): + "length of dictwrapper" + return len(self.__dict__) + +# +# Someday a cleaner Python interface using this machinery will live here +# + +# End +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/clienthelpers.py b/tools/gps/clienthelpers.py new file mode 100644 index 00000000..e0cd2c8e --- /dev/null +++ b/tools/gps/clienthelpers.py @@ -0,0 +1,920 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# clienthelpers.py - helper functions for xgps and test_maidenhead +# This duplicates gpsclient.c, but in python. Keep the two files in +# sync. +# +# See gpsclient.c for code comments. +# +# This file is Copyright 2019 by the GPSD project +# SPDX-License-Identifier: BSD-2-Clause +"""GPSd client helpers submodule.""" +import math +import os + + +TABLE_ROWS = 37 +TABLE_COLS = 73 +TABLE_SPAN = 5 + +GEOID_DELTA = [ + # -90 + [ -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, + -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, + -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, + -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, + -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, + -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, + -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, + -3015, -3015, -3015], + # -85 + [ -3568, -3608, -3739, -3904, -4039, -4079, -4033, -3946, -3845, -3734, + -3603, -3458, -3310, -3163, -2994, -2827, -2695, -2667, -2737, -2823, + -2840, -2757, -2634, -2567, -2547, -2540, -2452, -2247, -1969, -1704, + -1540, -1507, -1552, -1592, -1573, -1513, -1465, -1478, -1542, -1577, + -1483, -1256, -1029, -957, -1066, -1216, -1262, -1194, -1118, -1129, + -1231, -1370, -1504, -1641, -1813, -2028, -2255, -2455, -2630, -2811, + -3022, -3242, -3436, -3578, -3658, -3676, -3640, -3578, -3527, -3490, + -3532, -3570, -3568], + # -80 + [ -5232, -5276, -5275, -5301, -5286, -5276, -5218, -5001, -4775, -4580, + -4319, -4064, -3854, -3691, -3523, -3205, -2910, -2608, -2337, -2355, + -2417, -2445, -2471, -2350, -2230, -2136, -1869, -1689, -1732, -1748, + -1540, -1236, -1048, -794, -569, -603, -501, -305, -166, 19, + 146, 274, 444, 510, 534, 550, 458, 373, 473, 575, + 607, 732, 562, 153, -271, -825, -1300, -1861, -2475, -2866, + -3434, -4001, -4196, -4533, -4989, -5152, -5094, -4983, -4987, -5065, + -5055, -5115, -5232], + # -75 + [ -6155, -6339, -6266, -6344, -6282, -6100, -6009, -5492, -5088, -4547, + -4187, -3901, -3586, -3234, -3051, -2886, -2577, -2289, -1981, -1655, + -1435, -1096, -557, -617, -998, -961, -655, -464, -170, 79, + -103, -64, 150, 223, 819, 1006, 1174, 1136, 1211, 1278, + 1467, 1686, 1783, 1706, 1833, 1721, 1653, 1580, 1267, 953, + 629, 807, 774, 607, 217, -386, -814, -1354, -2452, -3542, + -3833, -3932, -4259, -4962, -4977, -5536, -5753, -5800, -6012, -5835, + -5751, -5820, -6155], + # -70 + [ -6218, -6432, -6333, -6150, -6021, -5948, -5705, -5480, -5213, -4789, + -4365, -4003, -3757, -3514, -3250, -3000, -2672, -2541, -2138, -1220, + -844, -277, 249, 906, 458, 69, 26, 98, 166, 130, + 118, 253, 303, 437, 1010, 1341, 1423, 1558, 1682, 1825, + 1766, 1917, 2027, 2047, 2164, 2909, 2882, 2997, 3010, 2687, + 1749, 1703, 1799, 1438, 1099, 346, -813, -1432, -2149, -2320, + -2704, -3085, -3907, -4172, -4287, -4846, -5466, -5592, -5576, -5525, + -5800, -5954, -6218], + # -65 + [ -5152, -5115, -5049, -4943, -4858, -4714, -4580, -4369, -4202, -4060, + -3806, -3454, -3210, -3007, -2749, -2484, -2264, -1928, -1501, -1113, + -614, 31, 642, 1502, 1833, 1844, 1268, 1442, 1441, 1302, + 1164, 1041, 945, 874, 896, 1059, 1368, 1680, 1736, 1975, + 1891, 1979, 2131, 2338, 2672, 2861, 3114, 3097, 2801, 2695, + 2422, 2022, 1648, 1340, 713, 352, -127, -895, -1740, -2040, + -2854, -3292, -3453, -3922, -4395, -4538, -4554, -4356, -4445, -4669, + -4988, -5122, -5152], + # -60 + [ -4598, -4449, -4278, -4056, -3732, -3417, -3205, -3094, -3008, -2876, + -2669, -2478, -2350, -2272, -2218, -1969, -1660, -1381, -1123, -716, + -350, 247, 924, 1712, 2016, 2066, 2032, 1556, 2123, 2322, + 2384, 1034, 2121, 1923, 1720, 1571, 1517, 1668, 2008, 2366, + 2546, 2736, 2914, 3169, 3395, 3467, 3315, 3286, 3279, 3073, + 2930, 2727, 2502, 1783, 893, 311, -328, -778, -1364, -1973, + -2467, -2833, -3143, -3283, -3311, -3120, -2956, -3027, -3485, -3972, + -4454, -4679, -4598], + # -55 + [ -3414, -3429, -3223, -3013, -2704, -2474, -2292, -2185, -1962, -1818, + -1828, -1485, -1259, -1284, -1327, -1304, -1097, -1071, -628, -326, + 174, 340, 1331, 1217, 1712, 1441, 1467, 1578, 1654, 2179, + 764, 1486, 2074, 2245, 2462, 2655, 2720, 2581, 2423, 2731, + 3145, 3383, 3436, 3909, 4448, 4422, 4032, 3938, 3665, 3461, + 3465, 3317, 2487, 1908, 1311, 683, 52, -582, -1196, -1798, + -2158, -2450, -2475, -2429, -2277, -2011, -2140, -2306, -2551, -2726, + -3016, -3319, -3414], + # -50 + [ -1615, -1938, -1875, -1827, -1839, -1793, -1605, -1650, -1737, -1773, + -1580, -1237, -1010, -983, -1051, -1025, -838, -653, -316, 48, + 502, 1382, 1186, 1114, 1264, 785, 231, 329, 353, 556, + 1084, 1597, 2065, 2475, 2744, 2701, 2518, 2545, 2584, 2963, + 3323, 3537, 3792, 4085, 4520, 4505, 4459, 4287, 3818, 4112, + 3975, 3293, 2748, 2043, 1272, 569, -207, -898, -1498, -1990, + -2242, -2358, -2212, -1968, -1843, -1695, -1705, -1688, -1400, -1177, + -1013, -1168, -1615], + # -45 + [ 338, -20, -606, -849, -777, -838, -1123, -1322, -1485, -1503, + -1413, -1203, -1077, -1004, -960, -829, -662, -371, -88, 322, + 710, 1323, 1831, 1202, 908, 47, -292, -367, -495, -174, + 688, 1500, 2194, 2673, 2568, 2423, 2099, 2168, 2617, 2834, + 3254, 3328, 3443, 4442, 4639, 4588, 4524, 4223, 3575, 3187, + 3101, 2651, 2155, 1506, 774, -55, -961, -1719, -2355, -2719, + -2731, -2670, -2430, -2026, -1715, -1477, -1144, -901, -646, -303, + 871, 565, 338], + # -40 + [ 2048, 1283, 637, 317, 109, -156, -679, -1023, -1186, -1277, + -1275, -1202, -1282, -1150, -1022, -881, -690, -300, -84, 130, + 694, 937, 2220, 1511, 1341, 558, -266, -623, -670, -209, + 643, 1459, 2101, 2385, 2307, 2000, 1765, 1992, 2496, 2733, + 2941, 3431, 3298, 3327, 3877, 4306, 4069, 3446, 2844, 2601, + 2333, 1786, 1318, 599, -238, -1184, -2098, -2786, -3250, -3406, + -3351, -3095, -2741, -2101, -1482, -148, -201, 221, 491, 1179, + 1877, 1206, 2048], + # -35 + [ 2833, 2556, 1700, 1059, 497, -21, -370, -752, -959, -1103, + -1093, -1104, -1198, -1097, -960, -785, -596, -362, -211, 103, + 739, 1300, 3029, 2021, 1712, 1269, -23, -616, -701, -255, + 684, 1237, 1701, 1903, 1696, 1789, 1795, 2034, 2398, 2561, + 3187, 2625, 2609, 2897, 2564, 3339, 3118, 3121, 2240, 2102, + 1529, 991, 387, -559, -1464, -2380, -3138, -3999, -3899, -3446, + -3473, -3300, -2823, -1043, 143, 970, 2058, 1555, 1940, 2621, + 3154, 3839, 2833], + # -30 + [ 4772, 3089, 2257, 1381, 566, 64, -136, -612, -868, -1186, + -1309, -1131, -1033, -903, -780, -625, -443, -242, 100, 269, + 815, 1489, 3633, 2424, 1810, 1138, 297, -720, -847, -2, + 347, 579, 1025, 1408, 1504, 1686, 2165, 2353, 2599, 3182, + 3332, 3254, 3094, 2042, 1369, 1945, 1468, 1487, 1505, 1048, + 613, 26, -904, -1757, -2512, -3190, -3751, -3941, -3939, -2896, + -2222, -1766, -1442, 70, 1262, 2229, 3189, 2910, 3371, 3608, + 4379, 4520, 4772], + # -25 + [ 4984, 2801, 2475, 1374, 798, 198, -269, -628, -1063, -1262, + -1090, -970, -692, -516, -458, -313, -143, 19, 183, 403, + 837, 1650, 3640, 2990, 2084, 628, 422, -597, -1130, -712, + -474, -110, 446, 1043, 1349, 1571, 2008, 2572, 2405, 3175, + 2766, 2407, 2100, 1130, 367, 840, 89, 114, 49, -25, + -494, -1369, -2345, -3166, -3804, -4256, -4141, -3730, -3337, -1814, + -901, -388, 298, 1365, 2593, 3490, 4639, 4427, 4795, 4771, + 5325, 5202, 4984], + # -20 + [ 4994, 5152, 2649, 1466, 935, 427, -115, -518, -838, -1135, + -1134, -917, -525, -280, -218, -310, -396, -306, -137, 148, + 811, 1643, 3496, 4189, 1958, 358, -784, -684, -740, -800, + -579, -638, -49, 704, 1221, 1358, 1657, 1957, 2280, 2639, + 2157, 1246, 728, -364, -1021, -586, -1098, -1055, -1032, -1244, + -2065, -3158, -4028, -4660, -4802, -4817, -4599, -3523, -2561, -1260, + 446, 1374, 2424, 3310, 4588, 5499, 5724, 5479, 5698, 5912, + 6400, 6116, 4994], + # -15 + [ 4930, 4158, 2626, 1375, 902, 630, 150, -275, -667, -1005, + -954, -847, -645, -376, -315, -479, -639, -681, -550, -268, + 709, 2996, 4880, 2382, 1695, -136, -964, -1211, -1038, -1045, + -695, -595, 23, 733, 1107, 1318, 1348, 1376, 1630, 2240, + 1248, 454, -737, -1252, -2001, -2513, -1416, -2169, -2269, -3089, + -4063, -5194, -5715, -6105, -5700, -4873, -3919, -2834, -1393, -112, + 1573, 3189, 3907, 4863, 5437, 6548, 6379, 6281, 6289, 5936, + 6501, 5794, 4930], + # -10 + [ 3525, 2747, 2135, 1489, 1078, 739, 544, -39, -268, -588, + -917, -1025, -1087, -940, -771, -923, -1177, -1114, -919, -383, + -108, 2135, 2818, 1929, 386, -1097, -1911, -1619, -1226, -1164, + -952, -583, 399, 1070, 1280, 1345, 1117, 993, 1306, 1734, + 538, -463, -1208, -1602, -2662, -3265, -3203, -3408, -3733, -5014, + -6083, -7253, -7578, -7096, -6418, -4658, -2647, -586, -87, 1053, + 3840, 3336, 5240, 6253, 6898, 7070, 7727, 7146, 6209, 5826, + 5068, 4161, 3525], + # -5 + [ 2454, 1869, 1656, 1759, 1404, 1263, 1012, 605, 108, -511, + -980, -1364, -1620, -1633, -1421, -1342, -1412, -1349, -1006, -229, + 1711, 1293, 1960, 605, -793, -2058, -2108, -2626, -1195, -606, + -513, -108, 671, 1504, 1853, 1711, 1709, 940, 570, 296, + -913, -1639, -1471, -1900, -3000, -4164, -4281, -4062, -5366, -6643, + -7818, -8993, -9275, -8306, -6421, -4134, -1837, 1367, 2850, 4286, + 5551, 5599, 5402, 6773, 7736, 7024, 8161, 6307, 5946, 4747, + 3959, 3130, 2454], + # 0 + [ 2128, 1774, 1532, 1470, 1613, 1589, 1291, 783, 79, -676, + -1296, -1941, -2298, -2326, -2026, -1738, -1412, -1052, -406, 82, + 1463, 1899, 1352, -170, -1336, -2446, -2593, -2328, -1863, -833, + 245, 1005, 1355, 1896, 1913, 1888, 1723, 1642, 940, -127, + -1668, -1919, -1078, -1633, -2762, -4357, -4885, -5143, -6260, -7507, + -8947, -10042, -10259, -8865, -6329, -3424, -692, 1445, 3354, 5132, + 5983, 4978, 7602, 7274, 7231, 6941, 6240, 5903, 4944, 4065, + 3205, 2566, 2128], + # 5 + [ 1632, 1459, 1243, 1450, 1643, 1432, 867, 283, -420, -1316, + -1993, -2614, -3012, -3016, -2555, -1933, -1256, -688, -133, 634, + 1369, 2095, -92, -858, -1946, -3392, -3666, -3110, -1839, -371, + 674, 1221, 1657, 1994, 2689, 2577, 2020, 2126, 1997, 987, + -739, -989, -1107, -1369, -1914, -3312, -4871, -5365, -6171, -7732, + -9393, -10088, -10568, -9022, -6053, -4104, -1296, 373, 2310, 4378, + 6279, 6294, 6999, 6852, 6573, 6302, 5473, 5208, 4502, 3445, + 2790, 2215, 1632], + # 10 + [ 1285, 1050, 1212, 1439, 1055, 638, 140, -351, -1115, -2060, + -2904, -3593, -3930, -3694, -2924, -2006, -1145, -441, 164, 1059, + 91, -440, -1043, -2791, -4146, -4489, -4259, -3218, -1691, -683, + 306, 1160, 1735, 3081, 3275, 2807, 2373, 2309, 2151, 1245, + 207, -132, -507, -564, -956, -1917, -3167, -5067, -5820, -7588, + -9107, -9732, -9732, -8769, -6308, -4585, -2512, -891, 1108, 3278, + 5183, 6391, 5985, 5969, 6049, 5616, 4527, 4156, 3531, 2776, + 2456, 1904, 1285], + # 15 + [ 862, 804, 860, 969, 544, 89, -417, -1008, -1641, -2608, + -3607, -4234, -4482, -4100, -3232, -2092, -1105, -1092, 238, 330, + -571, -1803, -2983, -3965, -5578, -4864, -3777, -2572, -1690, -536, + 806, 2042, 2323, 3106, 3019, 2833, 2260, 2064, 2036, 1358, + 1030, 908, 391, -54, -377, -885, -2172, -3359, -5309, -6686, + -8058, -8338, -8695, -8322, -6404, -5003, -3420, -2060, -255, 1833, + 4143, 4218, 4771, 5031, 5241, 5504, 4399, 3471, 2832, 2266, + 1643, 1190, 862], + # 20 + [ 442, 488, 986, 877, 757, 1175, -696, -1473, -2285, -3128, + -3936, -4520, -4739, -4286, -3350, -2092, -747, -1894, -1083, -1508, + -2037, -2528, -4813, -6316, -4698, -4222, -3279, -1814, -1001, 212, + 1714, 2273, 2535, 3367, 3112, 2736, 3086, 2742, 2679, 2071, + 1422, 1333, 922, 619, 183, -945, -3070, -3680, -4245, -5461, + -6064, -6652, -6806, -6210, -5947, -5177, -3814, -2589, -1319, 551, + 2150, 3262, 3799, 4177, 4898, 4658, 4149, 2833, 2148, 1410, + 899, 551, 442], + # 25 + [ -248, 12, 716, 415, 327, -187, -1103, -1729, -2469, -3296, + -4040, -4545, -4642, -4232, -3466, -2064, -1667, -3232, -2660, -2685, + -2789, -4262, -5208, -5084, -4935, -4077, -2622, -804, 131, 946, + 1859, 2203, 3038, 3433, 3758, 3029, 2757, 3524, 3109, 2511, + 2300, 1554, 1316, 1114, 954, -81, -2642, -3389, -3167, -4211, + -4634, -5193, -6014, -6245, -5347, -5313, -3846, -3149, -2130, -354, + 1573, 2760, 3310, 3713, 4594, 3862, 2827, 1939, 1019, 313, + -142, -378, -248], + # 30 + [ -720, -717, -528, -573, -867, -1224, -1588, -2135, -2796, -3432, + -4036, -4329, -4246, -3464, -2996, -2389, -2323, -2844, -2744, -2884, + -3238, -4585, -5164, -4463, -4064, -3238, -1751, 150, 1657, 2501, + 3023, 3007, 3404, 3976, 4354, 4648, 3440, 2708, 2813, 2968, + 2611, 2104, 1606, 1808, 1086, -392, -1793, -689, -1527, -2765, + -3766, -4709, -3687, -2800, -3375, -3793, -3365, -4182, -2385, -1115, + 785, 2302, 3020, 3564, 4178, 2993, 1940, 1081, 331, -364, + -683, -690, -720], + # 35 + [ -1004, -1222, -1315, -1304, -1463, -1680, -2160, -2675, -3233, -3746, + -4021, -4053, -3373, -3012, -2447, -2184, -2780, -3219, -2825, -3079, + -3181, -4284, -4548, -3867, -3123, -2302, -785, 943, 2687, 4048, + 4460, 4290, 4118, 4585, 4282, 4437, 4898, 3818, 3696, 3414, + 2299, 2057, 627, 1915, 1833, 451, 678, -876, -1602, -2167, + -3344, -2549, -2860, -3514, -4043, -4207, -4005, -3918, -3121, -1521, + 471, 2023, 2980, 3679, 3465, 2405, 1475, 553, -142, -880, + -1178, -963, -1004], + # 40 + [ -1223, -1218, -1076, -1116, -1298, -1541, -2085, -2648, -3120, -3473, + -3679, -3342, -2334, -1912, -1787, -1756, -2482, -3182, -3322, -3429, + -3395, -3374, -3372, -3341, -2654, -1509, 105, 1620, 3250, 4603, + 5889, 5776, 5198, 4840, 4903, 5370, 5086, 4536, 4519, 4601, + 3395, 4032, 3890, 3537, 3113, 2183, -1769, -1552, -2856, -3694, + -4092, -3614, -5468, -6518, -6597, -5911, -5476, -4465, -2802, -1076, + 232, 1769, 2305, 3018, 3768, 1721, 1694, 667, -154, -799, + -1068, -1196, -1223], + # 45 + [ -634, -460, -330, -267, -413, -818, -1310, -1763, -2352, -2738, + -2632, -2685, -1929, -1340, -737, -1441, -2254, -2685, -3358, -3488, + -3635, -3187, -2665, -2142, -1515, -124, 1727, 2798, 3965, 5065, + 6150, 6513, 6089, 5773, 5044, 4471, 4677, 5052, 3938, 4537, + 4425, 3652, 3063, 2178, 1267, 84, -1109, -1974, -2905, -3650, + -4264, -4741, -4136, -6324, -5826, -5143, -4851, -4344, -3225, -1386, + 5, 1153, 2198, 2833, 2835, 2563, 1337, 1194, 503, -329, + -289, -754, -634], + # 50 + [ -578, -40, 559, 880, 749, 464, 0, -516, -1140, -1655, + -1818, -1589, -1555, -1337, -1769, -1919, -2372, -2981, -3485, -3976, + -3941, -3565, -2614, -2223, -1253, 802, 2406, 3239, 4434, 5428, + 6265, 6394, 6180, 5690, 5855, 5347, 4506, 4685, 4799, 4445, + 3972, 3165, 2745, 1601, 1084, 41, -1170, -1701, -1916, -2914, + -3305, -3790, -4435, -4128, -4163, -4535, -4190, -3891, -2951, -1869, + -414, 851, 1494, 2097, 2268, 1939, 2031, 2460, 638, 578, + 325, 98, -578], + # 55 + [ -18, 482, 905, 1562, 1739, 983, 1097, 568, 34, -713, + -695, -1072, -1576, -1879, -2479, -2884, -3275, -3971, -4456, -4654, + -4461, -3688, -2697, -1623, -823, 1270, 2523, 3883, 4967, 5977, + 6049, 6149, 6095, 5776, 5820, 5575, 4642, 4099, 4025, 3462, + 2679, 2447, 1951, 1601, 1151, 663, 157, -603, -952, -1987, + -2609, -3316, -3600, -3684, -3717, -3836, -4024, -3452, -2950, -1861, + -903, 89, 975, 1499, 1560, 1601, 1922, 2031, 2326, -58, + 506, -177, -18], + # 60 + [ 93, 673, 969, 1168, 1498, 1486, 1439, 1165, 1128, 720, + 5, -689, -1610, -2409, -3094, -3585, -4193, -4772, -4678, -4521, + -4184, -2955, -2252, -834, 503, 1676, 2882, 4130, 4892, 5611, + 6390, 6338, 6069, 5974, 5582, 5461, 4788, 4503, 4080, 2957, + 1893, 1773, 1586, 1544, 1136, 1026, 622, 50, -389, -1484, + -2123, -2625, -3028, -3143, -3366, -3288, -3396, -3069, -2770, -2605, + -1663, -555, 25, 491, 1168, 1395, 1641, 1597, 1426, 1299, + 921, -160, 93], + # 65 + [ 419, 424, 443, 723, 884, 1030, 1077, 1191, 1065, 734, + 265, -1052, -1591, -2136, -2773, -3435, -3988, -3978, -3698, -3509, + -3370, -2490, -1347, -263, 1647, 2582, 3291, 4802, 4447, 5609, + 5879, 6454, 6709, 6606, 5988, 5365, 5103, 4385, 3996, 3250, + 2526, 1766, 1817, 1751, 1275, 857, 636, 29, -12, -918, + -1364, -1871, -2023, -2102, -2258, -2441, -2371, -2192, -1908, -1799, + -1720, -1662, -385, 86, 466, 880, 715, 834, 1010, 1105, + 877, 616, 419], + # 70 + [ 242, 93, 98, 62, -54, -25, -127, -156, -253, -412, + -805, -1106, -1506, -1773, -2464, -2829, -2740, -2579, -2559, -2271, + -1849, -853, 294, 1055, 2357, 2780, 2907, 3909, 4522, 5272, + 5594, 5903, 5966, 5930, 5592, 5188, 4878, 4561, 4190, 3834, + 2963, 2451, 1981, 1525, 1064, 694, 253, -70, -318, -781, + -979, -1048, -1274, -1413, -1175, -1313, -1449, -1206, -850, -1087, + -828, -933, -540, -301, -35, 53, 279, 267, 345, 371, + 334, 289, 242], + # 75 + [ 128, 228, 376, 46, -173, -355, -417, -548, -764, -925, + -419, -950, -1185, -1102, -1293, -1355, -1075, -713, -365, 167, + 516, 1381, 1882, 1826, 1956, 2492, 3192, 3541, 3750, 4123, + 4462, 4592, 4472, 4705, 4613, 4559, 4340, 4392, 4144, 3973, + 3119, 2582, 2057, 1684, 1199, 834, 477, 325, 295, -198, + -459, -670, -706, -677, -766, -852, -939, -905, -637, -601, + -531, -433, -292, -158, 88, 85, 118, 121, 147, 179, + 173, 149, 128], + # 80 + [ 342, 293, 244, 159, 38, 20, 15, -15, -109, -119, + -240, -182, 16, 397, 550, 264, 350, 670, 865, 681, + 1188, 1136, 703, 1153, 1930, 2412, 2776, 3118, 3351, 3634, + 3653, 3272, 3177, 3161, 3354, 3671, 3615, 3572, 3522, 3274, + 2914, 2682, 2426, 2185, 1845, 1584, 1297, 1005, 809, 507, + 248, 314, 230, 96, 149, 240, 274, 297, 153, 109, + 164, 91, 104, 43, 12, 153, 243, 170, 184, 59, + 99, 158, 342], + # 85 + [ 912, 961, 1013, 1013, 997, 1032, 1026, 1050, 1072, 1132, + 1156, 1253, 1310, 1389, 1441, 1493, 1508, 1565, 1621, 1642, + 1768, 1888, 2036, 2089, 2117, 2106, 2010, 2120, 2276, 2376, + 2426, 2427, 2526, 2582, 2493, 2534, 2628, 2564, 2471, 2509, + 2407, 2332, 2214, 2122, 1987, 1855, 1714, 1619, 1517, 1474, + 1406, 1351, 1308, 1264, 1181, 1081, 1047, 1084, 1043, 964, + 851, 755, 732, 706, 697, 785, 864, 762, 686, 729, + 789, 856, 912], + # 90 + [ 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, + 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, + 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, + 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, + 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, + 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, + 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, 1490, + 1490, 1490, 1490]] + + +# This table is wmm2015. Values obtained from MagneticField, part of +# geographiclib., by using devtools/get_mag_var_table.py +# +# magvar[][] has the magnetic variation (declination), in hundreths of +# a degree, on a 5 degree by 5 * degree grid for the entire planet. +# +# This table is duplicated in geoid.c. Keep them in sync. +# + +magvar_table = [ + # -90 + [ 14920, 14420, 13920, 13420, 12920, 12420, 11920, 11420, 10920, + 10420, 9920, 9420, 8920, 8420, 7920, 7420, 6920, 6420, + 5920, 5420, 4920, 4420, 3920, 3420, 2920, 2420, 1920, + 1420, 920, 420, -80, -580, -1080, -1580, -2080, -2580, + -3080, -3580, -4080, -4580, -5080, -5580, -6080, -6580, -7080, + -7580, -8080, -8580, -9080, -9580, -10080, -10580, -11080, -11580, + -12080, -12580, -13080, -13580, -14080, -14580, -15080, -15580, -16080, + -16580, -17080, -17580, 17920, 17420, 16920, 16420, 15920, 15420, + 14920], + # -85 + [ 14174, 13609, 13052, 12504, 11965, 11435, 10914, 10402, 9898, + 9403, 8915, 8434, 7960, 7492, 7029, 6572, 6119, 5671, + 5226, 4784, 4346, 3909, 3475, 3042, 2611, 2180, 1750, + 1319, 889, 457, 25, -409, -845, -1283, -1723, -2166, + -2612, -3061, -3514, -3971, -4431, -4895, -5364, -5837, -6315, + -6798, -7285, -7778, -8277, -8781, -9291, -9808, -10331, -10860, + -11397, -11940, -12491, -13048, -13613, -14184, -14761, -15344, -15931, + -16523, -17117, -17714, 17690, 17094, 16500, 15910, 15325, 14746, + 14174], + # -80 + [ 12958, 12331, 11733, 11163, 10619, 10099, 9600, 9121, 8659, + 8211, 7776, 7352, 6937, 6529, 6127, 5730, 5338, 4949, + 4563, 4180, 3799, 3420, 3043, 2667, 2292, 1918, 1544, + 1170, 795, 419, 40, -342, -727, -1117, -1512, -1912, + -2318, -2729, -3147, -3571, -4002, -4438, -4880, -5328, -5782, + -6241, -6707, -7180, -7659, -8146, -8641, -9146, -9662, -10190, + -10732, -11290, -11866, -12461, -13077, -13716, -14379, -15064, -15772, + -16500, -17244, -17999, 17241, 16485, 15738, 15007, 14298, 13614, + 12958], + # -75 + [ 11045, 10435, 9882, 9378, 8915, 8485, 8081, 7699, 7335, + 6983, 6640, 6304, 5972, 5642, 5313, 4982, 4651, 4317, + 3982, 3646, 3309, 2972, 2635, 2300, 1966, 1634, 1304, + 975, 646, 316, -16, -352, -694, -1042, -1398, -1764, + -2138, -2523, -2916, -3319, -3729, -4146, -4569, -4997, -5430, + -5866, -6306, -6750, -7197, -7650, -8109, -8576, -9054, -9545, + -10054, -10585, -11143, -11736, -12371, -13058, -13806, -14624, -15519, + -16489, -17525, 17396, 16307, 15245, 14242, 13317, 12478, 11723, + 11045], + # -70 + [ 8567, 8144, 7771, 7437, 7132, 6851, 6587, 6336, 6095, + 5858, 5623, 5386, 5144, 4896, 4639, 4372, 4094, 3807, + 3510, 3206, 2895, 2582, 2266, 1952, 1641, 1334, 1033, + 736, 444, 155, -136, -429, -729, -1038, -1359, -1693, + -2042, -2406, -2782, -3170, -3568, -3973, -4383, -4795, -5208, + -5620, -6030, -6437, -6843, -7246, -7650, -8054, -8463, -8880, + -9308, -9756, -10230, -10744, -11314, -11965, -12737, -13686, -14889, + -16424, 17708, 15701, 13867, 12382, 11234, 10344, 9636, 9056, + 8567], + # -65 + [ 6318, 6126, 5946, 5777, 5617, 5466, 5322, 5183, 5046, + 4910, 4770, 4622, 4464, 4291, 4101, 3891, 3661, 3411, + 3142, 2857, 2558, 2250, 1938, 1625, 1318, 1019, 730, + 454, 189, -67, -318, -570, -827, -1096, -1381, -1685, + -2009, -2354, -2717, -3095, -3485, -3881, -4279, -4676, -5068, + -5453, -5828, -6193, -6546, -6887, -7218, -7538, -7849, -8152, + -8450, -8744, -9037, -9335, -9645, -9979, -10368, -10886, -11809, + -15193, 10922, 8778, 8037, 7595, 7264, 6988, 6744, 6523, + 6318], + # -60 + [ 4768, 4709, 4640, 4566, 4490, 4416, 4344, 4274, 4208, + 4141, 4071, 3993, 3902, 3794, 3663, 3505, 3318, 3100, + 2852, 2578, 2280, 1965, 1640, 1313, 991, 682, 392, + 123, -125, -354, -569, -778, -991, -1215, -1458, -1726, + -2023, -2347, -2695, -3064, -3445, -3833, -4220, -4600, -4969, + -5323, -5658, -5972, -6264, -6532, -6776, -6993, -7181, -7338, + -7458, -7532, -7546, -7474, -7273, -6857, -6058, -4557, -2058, + 727, 2633, 3688, 4259, 4572, 4736, 4812, 4830, 4812, + 4768], + # -55 + [ 3771, 3768, 3750, 3720, 3685, 3647, 3611, 3577, 3546, + 3519, 3492, 3462, 3421, 3364, 3283, 3171, 3023, 2835, + 2607, 2339, 2036, 1705, 1356, 1001, 652, 319, 13, + -261, -501, -709, -891, -1058, -1222, -1394, -1587, -1810, + -2069, -2365, -2694, -3048, -3419, -3795, -4168, -4529, -4871, + -5189, -5479, -5737, -5961, -6148, -6293, -6394, -6445, -6436, + -6355, -6184, -5896, -5455, -4817, -3939, -2818, -1532, -251, + 866, 1747, 2404, 2878, 3214, 3446, 3600, 3697, 3750, + 3771], + # -50 + [ 3099, 3120, 3123, 3115, 3098, 3077, 3056, 3036, 3021, + 3011, 3006, 3001, 2992, 2971, 2929, 2856, 2743, 2584, + 2373, 2111, 1800, 1449, 1071, 680, 294, -71, -401, + -686, -925, -1118, -1273, -1401, -1516, -1633, -1767, -1932, + -2138, -2392, -2689, -3021, -3374, -3734, -4089, -4425, -4736, + -5014, -5253, -5450, -5601, -5700, -5743, -5724, -5634, -5465, + -5202, -4832, -4344, -3732, -3008, -2203, -1368, -556, 188, + 839, 1387, 1838, 2201, 2485, 2704, 2865, 2979, 3054, + 3099], + # -45 + [ 2611, 2642, 2656, 2658, 2651, 2639, 2625, 2610, 2598, + 2592, 2591, 2595, 2601, 2600, 2583, 2539, 2456, 2321, + 2128, 1871, 1554, 1183, 774, 346, -76, -472, -823, + -1120, -1357, -1540, -1675, -1774, -1850, -1915, -1987, -2083, + -2220, -2411, -2657, -2951, -3276, -3612, -3941, -4248, -4522, + -4755, -4940, -5073, -5147, -5158, -5102, -4972, -4762, -4467, + -4083, -3612, -3068, -2474, -1858, -1251, -675, -141, 343, + 777, 1161, 1495, 1781, 2020, 2214, 2366, 2479, 2559, + 2611], + # -40 + [ 2236, 2269, 2287, 2295, 2295, 2289, 2279, 2266, 2253, + 2243, 2238, 2239, 2245, 2250, 2246, 2220, 2157, 2043, + 1864, 1613, 1290, 903, 468, 12, -438, -854, -1216, + -1513, -1745, -1916, -2038, -2121, -2172, -2202, -2220, -2243, + -2294, -2397, -2566, -2798, -3077, -3376, -3672, -3945, -4180, + -4367, -4499, -4570, -4575, -4509, -4370, -4157, -3867, -3504, + -3076, -2600, -2099, -1602, -1131, -697, -301, 62, 396, + 706, 992, 1252, 1485, 1688, 1860, 1999, 2106, 2183, + 2236], + # -35 + [ 1934, 1966, 1985, 1996, 2001, 2000, 1994, 1982, 1967, + 1952, 1939, 1932, 1930, 1932, 1930, 1911, 1860, 1758, + 1589, 1343, 1016, 618, 167, -305, -767, -1187, -1544, + -1830, -2047, -2204, -2315, -2387, -2426, -2432, -2406, -2360, + -2315, -2308, -2368, -2508, -2716, -2964, -3222, -3462, -3666, + -3818, -3909, -3934, -3888, -3771, -3584, -3331, -3015, -2645, + -2236, -1809, -1390, -1003, -658, -355, -85, 165, 403, + 634, 856, 1066, 1261, 1436, 1588, 1714, 1813, 1885, + 1934], + # -30 + [ 1687, 1715, 1732, 1744, 1752, 1755, 1753, 1744, 1728, + 1709, 1689, 1672, 1660, 1653, 1647, 1629, 1581, 1485, + 1321, 1075, 746, 343, -113, -588, -1045, -1453, -1792, + -2056, -2251, -2390, -2484, -2542, -2565, -2544, -2475, -2360, + -2220, -2093, -2026, -2049, -2164, -2349, -2568, -2786, -2976, + -3116, -3193, -3201, -3138, -3007, -2815, -2567, -2271, -1936, + -1577, -1218, -883, -590, -345, -140, 41, 212, 385, + 563, 742, 918, 1085, 1238, 1374, 1488, 1578, 1643, + 1687], + # -25 + [ 1485, 1507, 1520, 1530, 1539, 1544, 1545, 1539, 1525, + 1504, 1479, 1454, 1434, 1420, 1407, 1385, 1336, 1239, + 1074, 827, 497, 93, -359, -823, -1262, -1647, -1960, + -2198, -2366, -2478, -2546, -2575, -2564, -2502, -2380, -2200, + -1978, -1753, -1575, -1486, -1504, -1617, -1794, -1998, -2189, + -2341, -2432, -2454, -2406, -2295, -2131, -1920, -1670, -1388, + -1089, -796, -531, -312, -141, -8, 108, 224, 353, + 495, 646, 798, 945, 1082, 1204, 1308, 1389, 1447, + 1485], + # -20 + [ 1322, 1337, 1344, 1350, 1356, 1363, 1366, 1363, 1351, + 1330, 1303, 1275, 1249, 1230, 1212, 1186, 1132, 1030, + 859, 609, 278, -121, -561, -1006, -1420, -1775, -2058, + -2264, -2401, -2478, -2506, -2489, -2425, -2309, -2134, -1903, + -1633, -1359, -1122, -961, -902, -945, -1074, -1254, -1445, + -1612, -1727, -1778, -1764, -1691, -1570, -1409, -1213, -986, + -743, -504, -294, -131, -15, 66, 135, 213, 311, + 430, 563, 699, 832, 957, 1070, 1165, 1240, 1291, + 1322], + # -15 + [ 1194, 1201, 1202, 1201, 1204, 1209, 1213, 1211, 1202, + 1183, 1157, 1128, 1101, 1080, 1059, 1028, 968, 858, + 680, 425, 94, -296, -718, -1138, -1522, -1846, -2096, + -2268, -2367, -2401, -2378, -2304, -2182, -2013, -1798, -1545, + -1270, -996, -752, -566, -462, -451, -529, -673, -849, + -1019, -1151, -1228, -1246, -1212, -1135, -1023, -876, -700, + -505, -311, -145, -23, 53, 97, 133, 184, 262, + 366, 488, 615, 739, 856, 963, 1054, 1124, 1170, + 1194], + # -10 + [ 1097, 1098, 1090, 1083, 1081, 1083, 1087, 1087, 1080, + 1063, 1039, 1011, 986, 965, 943, 906, 838, 718, + 531, 271, -56, -434, -835, -1226, -1578, -1869, -2085, + -2221, -2279, -2265, -2188, -2059, -1886, -1678, -1446, -1198, + -946, -703, -482, -302, -181, -135, -170, -275, -425, + -583, -719, -810, -851, -848, -808, -736, -633, -500, + -346, -191, -59, 32, 78, 96, 109, 141, 205, + 300, 415, 537, 658, 772, 877, 966, 1035, 1078, + 1097], + # -5 + [ 1026, 1022, 1009, 995, 988, 987, 990, 992, 987, + 973, 951, 926, 903, 882, 856, 813, 734, 603, + 407, 144, -179, -542, -919, -1281, -1601, -1858, -2038, + -2136, -2152, -2094, -1971, -1797, -1589, -1363, -1131, -903, + -685, -480, -293, -131, -10, 55, 50, -22, -143, + -284, -412, -507, -561, -578, -565, -525, -457, -360, + -242, -121, -18, 47, 71, 69, 66, 85, 140, + 229, 339, 460, 580, 695, 803, 895, 966, 1010, + 1026], + # 0 + [ 975, 971, 953, 935, 924, 922, 926, 930, 928, + 917, 898, 875, 852, 828, 796, 743, 651, 506, + 300, 34, -282, -629, -981, -1314, -1601, -1823, -1967, + -2028, -2006, -1910, -1753, -1553, -1327, -1096, -873, -669, + -484, -315, -160, -20, 94, 166, 181, 134, 38, + -84, -201, -293, -352, -380, -384, -367, -327, -261, + -175, -84, -9, 33, 39, 23, 8, 17, 64, + 147, 255, 375, 497, 617, 731, 831, 908, 957, + 975], + # 5 + [ 937, 936, 920, 901, 890, 888, 895, 902, 904, + 897, 880, 858, 832, 803, 761, 693, 584, 423, + 205, -65, -374, -704, -1031, -1333, -1587, -1773, -1881, + -1906, -1853, -1730, -1554, -1341, -1112, -885, -675, -490, + -331, -191, -62, 58, 162, 235, 260, 231, 154, + 50, -56, -142, -201, -234, -249, -248, -228, -188, + -130, -67, -18, 2, -9, -38, -64, -63, -24, + 52, 157, 276, 402, 529, 652, 763, 852, 911, + 937], + # 10 + [ 901, 911, 902, 889, 881, 883, 895, 908, 916, + 913, 899, 876, 845, 806, 749, 662, 532, 352, + 120, -155, -459, -773, -1076, -1347, -1565, -1715, -1787, + -1781, -1703, -1564, -1380, -1167, -942, -724, -526, -357, + -216, -96, 13, 117, 210, 280, 312, 294, 232, + 142, 47, -33, -89, -125, -145, -153, -149, -129, + -97, -61, -38, -40, -67, -110, -146, -156, -127, + -58, 40, 159, 289, 424, 559, 683, 787, 861, + 901], + # 15 + [ 859, 887, 893, 892, 893, 904, 924, 945, 959, + 962, 950, 925, 888, 836, 760, 651, 497, 294, + 45, -238, -540, -841, -1120, -1360, -1542, -1655, -1694, + -1661, -1565, -1418, -1234, -1028, -812, -604, -416, -257, + -128, -21, 75, 165, 249, 315, 349, 340, 290, + 212, 127, 53, 0, -35, -58, -73, -80, -78, + -68, -58, -61, -85, -131, -189, -238, -260, -243, + -185, -94, 22, 155, 298, 445, 584, 706, 799, + 859], + # 20 + [ 803, 856, 885, 903, 921, 945, 976, 1006, 1029, + 1037, 1027, 1000, 955, 888, 793, 658, 478, 250, + -18, -314, -617, -908, -1167, -1377, -1524, -1602, -1610, + -1554, -1446, -1296, -1117, -920, -715, -516, -336, -184, + -60, 40, 127, 208, 284, 346, 381, 379, 339, + 273, 198, 131, 81, 46, 22, 3, -14, -27, + -38, -54, -83, -131, -198, -273, -338, -374, -371, + -326, -244, -131, 3, 153, 310, 464, 605, 720, + 803], + # 25 + [ 731, 813, 871, 916, 957, 1000, 1045, 1086, 1117, + 1131, 1123, 1094, 1040, 958, 842, 682, 475, 221, + -70, -381, -690, -976, -1218, -1402, -1518, -1565, -1546, + -1472, -1354, -1203, -1030, -841, -646, -455, -281, -130, + -8, 90, 173, 249, 318, 377, 413, 418, 389, + 336, 273, 214, 167, 134, 107, 83, 57, 29, + -4, -45, -101, -176, -267, -361, -444, -497, -509, + -477, -404, -296, -162, -7, 159, 326, 483, 621, + 731], + # 30 + [ 643, 756, 848, 926, 996, 1062, 1124, 1178, 1217, + 1235, 1230, 1197, 1134, 1038, 902, 718, 484, 203, + -113, -443, -762, -1046, -1276, -1439, -1530, -1551, -1511, + -1422, -1297, -1145, -975, -792, -604, -418, -246, -96, + 29, 129, 213, 287, 354, 411, 450, 464, 449, + 411, 362, 313, 272, 238, 208, 176, 139, 94, + 38, -31, -117, -220, -337, -454, -556, -626, -654, + -634, -569, -466, -331, -174, -1, 176, 348, 506, + 643], + # 35 + [ 547, 691, 818, 930, 1031, 1123, 1205, 1273, 1321, + 1345, 1340, 1304, 1234, 1124, 968, 760, 499, 189, + -153, -505, -837, -1125, -1347, -1495, -1567, -1570, -1514, + -1413, -1281, -1126, -956, -776, -590, -406, -233, -80, + 50, 157, 246, 323, 392, 452, 498, 523, 525, + 507, 476, 440, 405, 371, 335, 292, 239, 173, + 90, -10, -129, -265, -411, -553, -675, -761, -801, + -792, -734, -633, -499, -338, -161, 25, 210, 386, + 547], + # 40 + [ 454, 626, 784, 929, 1061, 1180, 1283, 1366, 1424, + 1454, 1452, 1413, 1335, 1211, 1036, 804, 513, 173, + -199, -576, -925, -1219, -1439, -1577, -1637, -1627, -1560, + -1450, -1311, -1152, -978, -795, -607, -422, -245, -85, + 54, 172, 271, 358, 435, 504, 562, 603, 627, + 632, 622, 602, 575, 541, 497, 439, 365, 271, + 156, 19, -139, -312, -489, -657, -799, -900, -950, + -947, -892, -792, -656, -492, -310, -117, 79, 271, + 454], + # 45 + [ 374, 569, 754, 927, 1087, 1230, 1354, 1454, 1525, + 1563, 1565, 1525, 1439, 1301, 1105, 845, 522, 145, + -262, -668, -1037, -1341, -1562, -1696, -1748, -1729, -1654, + -1538, -1392, -1225, -1045, -855, -661, -469, -284, -114, + 39, 172, 289, 392, 486, 572, 648, 712, 761, + 793, 808, 807, 790, 756, 702, 627, 526, 397, + 241, 59, -144, -359, -573, -769, -930, -1043, -1099, + -1098, -1042, -940, -799, -630, -442, -241, -34, 172, + 374], + # 50 + [ 313, 526, 732, 928, 1110, 1276, 1419, 1536, 1622, + 1671, 1679, 1641, 1549, 1396, 1175, 882, 518, 96, + -355, -797, -1192, -1508, -1733, -1864, -1910, -1885, -1804, + -1680, -1526, -1350, -1159, -960, -755, -552, -354, -168, + 4, 160, 301, 430, 549, 661, 764, 857, 935, + 997, 1040, 1060, 1055, 1023, 960, 864, 731, 561, + 355, 119, -139, -405, -660, -886, -1066, -1187, -1245, + -1241, -1181, -1072, -925, -749, -552, -342, -125, 94, + 313], + # 55 + [ 270, 499, 721, 935, 1135, 1319, 1480, 1614, 1716, + 1779, 1797, 1761, 1664, 1495, 1245, 909, 490, 9, + -500, -988, -1413, -1744, -1971, -2099, -2138, -2105, -2015, + -1881, -1716, -1528, -1324, -1110, -890, -670, -454, -246, + -48, 137, 311, 475, 629, 776, 914, 1041, 1155, + 1250, 1322, 1366, 1377, 1351, 1281, 1164, 997, 779, + 514, 211, -114, -441, -744, -1003, -1201, -1329, -1387, + -1377, -1308, -1191, -1034, -849, -642, -422, -194, 38, + 270], + # 60 + [ 240, 482, 719, 946, 1162, 1361, 1539, 1690, 1808, + 1886, 1914, 1882, 1779, 1590, 1302, 908, 415, -149, + -734, -1280, -1738, -2081, -2305, -2422, -2447, -2399, -2294, + -2145, -1963, -1758, -1536, -1302, -1062, -819, -577, -340, + -110, 113, 326, 532, 730, 919, 1099, 1267, 1420, + 1551, 1657, 1730, 1764, 1749, 1680, 1548, 1349, 1080, + 747, 364, -44, -446, -809, -1108, -1328, -1464, -1519, + -1502, -1424, -1297, -1130, -934, -717, -487, -247, -4, + 240], + # 65 + [ 214, 468, 717, 957, 1186, 1399, 1592, 1759, 1891, + 1982, 2018, 1988, 1872, 1652, 1308, 830, 230, -448, + -1130, -1738, -2220, -2559, -2763, -2853, -2849, -2773, -2641, + -2467, -2260, -2030, -1782, -1523, -1255, -983, -709, -437, + -168, 96, 355, 608, 853, 1090, 1316, 1529, 1725, + 1899, 2044, 2155, 2220, 2232, 2178, 2047, 1828, 1514, + 1111, 635, 124, -376, -819, -1174, -1426, -1577, -1635, + -1615, -1530, -1394, -1218, -1013, -787, -546, -296, -42, + 214], + # 70 + [ 178, 442, 702, 955, 1195, 1421, 1625, 1802, 1944, + 2040, 2074, 2030, 1881, 1600, 1159, 546, -215, -1041, + -1821, -2461, -2924, -3215, -3362, -3394, -3337, -3214, -3040, + -2827, -2585, -2320, -2040, -1747, -1445, -1138, -828, -517, + -207, 101, 406, 706, 999, 1283, 1558, 1819, 2062, + 2284, 2479, 2637, 2752, 2811, 2799, 2700, 2496, 2170, + 1716, 1147, 511, -125, -689, -1136, -1450, -1637, -1714, + -1702, -1619, -1481, -1302, -1092, -859, -611, -352, -88, + 178], + # 75 + [ 114, 385, 652, 911, 1157, 1386, 1592, 1767, 1899, + 1975, 1973, 1868, 1622, 1193, 548, -307, -1284, -2226, + -2997, -3542, -3876, -4038, -4070, -4006, -3869, -3677, -3445, + -3182, -2895, -2589, -2269, -1939, -1600, -1257, -909, -559, + -210, 140, 486, 829, 1167, 1497, 1818, 2128, 2423, + 2698, 2951, 3173, 3356, 3491, 3562, 3550, 3431, 3177, + 2759, 2165, 1420, 605, -165, -797, -1254, -1540, -1684, + -1714, -1659, -1539, -1370, -1166, -936, -687, -426, -157, + 114], + # 80 + [ -8, 249, 501, 742, 965, 1164, 1327, 1442, 1489, + 1441, 1262, 902, 306, -553, -1618, -2716, -3648, -4317, + -4731, -4941, -4998, -4945, -4811, -4617, -4378, -4104, -3804, + -3483, -3146, -2796, -2436, -2068, -1693, -1315, -933, -549, + -164, 220, 603, 984, 1361, 1733, 2099, 2457, 2805, + 3140, 3459, 3758, 4031, 4273, 4472, 4616, 4686, 4657, + 4492, 4146, 3573, 2755, 1755, 731, -142, -778, -1181, + -1395, -1468, -1438, -1333, -1176, -980, -758, -517, -265, + -8], + # 85 + [ -546, -534, -552, -627, -794, -1097, -1585, -2300, -3236, + -4294, -5297, -6100, -6655, -6988, -7144, -7167, -7093, -6945, + -6742, -6496, -6217, -5912, -5585, -5241, -4883, -4513, -4134, + -3746, -3352, -2952, -2547, -2138, -1726, -1312, -896, -478, + -60, 359, 778, 1196, 1612, 2028, 2441, 2851, 3257, + 3660, 4057, 4448, 4831, 5205, 5567, 5916, 6248, 6558, + 6841, 7088, 7287, 7421, 7465, 7378, 7100, 6543, 5611, + 4289, 2795, 1493, 572, 0, -325, -488, -553, -561, + -546], + # 90 + [ -17825, -17325, -16825, -16325, -15825, -15325, -14825, -14325, -13825, + -13325, -12825, -12325, -11825, -11325, -10825, -10325, -9825, -9325, + -8825, -8325, -7825, -7325, -6825, -6325, -5825, -5325, -4825, + -4325, -3825, -3325, -2825, -2325, -1825, -1325, -825, -325, + 175, 675, 1175, 1675, 2175, 2675, 3175, 3675, 4175, + 4675, 5175, 5675, 6175, 6675, 7175, 7675, 8175, 8675, + 9175, 9675, 10175, 10675, 11175, 11675, 12175, 12675, 13175, + 13675, 14175, 14675, 15175, 15675, 16175, 16675, 17175, 17675, + -17825]] + +# "enum" for display units +unspecified = 0 +imperial = 1 +nautical = 2 +metric = 3 + +# "enum" for deg_to_str() conversion type +deg_dd = 0 +deg_ddmm = 1 +deg_ddmmss = 2 + + +def _non_finite(num): + """Is this number not finite?""" + return math.isnan(num) or math.isinf(num) + + +def deg_to_str(fmt, degrees): + """String-format a latitude/longitude.""" + try: + degrees = float(degrees) + except ValueError: + return '' + + if _non_finite(degrees): + return '' + + if degrees >= 360: + degrees -= 360 + if not math.fabs(degrees) <= 360: + return '' + + if fmt is deg_dd: + degrees += 1.0e-9 + return '%12.8f' % degrees + + degrees += 1.0e-8 / 36.0 + (fmin, fdeg) = math.modf(degrees) + + if fmt is deg_ddmm: + return '%3d %09.6f\'' % (fdeg, math.fabs(60. * fmin)) + + (fsec, fmin) = math.modf(60. * fmin) + return '%3d %02d\' %08.5f\"' % (fdeg, math.fabs(fmin), + math.fabs(60. * fsec)) + + +def gpsd_units(): + """Deduce a set of units from locale and environment.""" + unit_lookup = {'imperial': imperial, + 'metric': metric, + 'nautical': nautical} + if 'GPSD_UNITS' in os.environ: + store = os.environ['GPSD_UNITS'] + if isinstance(store, (str)) and store in unit_lookup: + return unit_lookup[store] + for inner in ['LC_MEASUREMENT', 'LANG']: + if inner in os.environ: + store = os.environ[inner] + if isinstance(store, (str)): + if store in ['C', 'POSIX'] or store.startswith('en_US'): + return imperial + return metric + return unspecified + + +# Arguments are in signed decimal latitude and longitude. For example, +# the location of Montevideo (GF15vc) is: -34.91, -56.21166 +# plagarized from https://ham.stackexchange.com/questions/221 +# by https://ham.stackexchange.com/users/10/walter-underwood-k6wru +def maidenhead(dec_lat, dec_lon): + """Convert latitude and longitude to Maidenhead grid locators.""" + try: + dec_lat = float(dec_lat) + dec_lon = float(dec_lon) + except ValueError: + return '' + if _non_finite(dec_lat) or _non_finite(dec_lon): + return '' + + if 90 < math.fabs(dec_lat) or 180 < math.fabs(dec_lon): + return '' + + if 89.99999 < dec_lat: + # force North Pole to just inside lat_sq 'R' + dec_lat = 89.99999 + + if 179.99999 < dec_lon: + # force 180 to just inside lon_sq 'R' + dec_lon = 179.99999 + + adj_lat = dec_lat + 90.0 + adj_lon = dec_lon + 180.0 + + # divide into 18 zones (fields) each 20 degrees lon, 10 degrees lat + grid_lat_sq = chr(int(adj_lat / 10) + 65) + grid_lon_sq = chr(int(adj_lon / 20) + 65) + + # divide into 10 zones (squares) each 2 degrees lon, 1 degrees lat + grid_lat_field = str(int(adj_lat % 10)) + grid_lon_field = str(int((adj_lon / 2) % 10)) + + # remainder in minutes + adj_lat_remainder = (adj_lat - int(adj_lat)) * 60 + adj_lon_remainder = ((adj_lon) - int(adj_lon / 2) * 2) * 60 + + # divide into 24 zones (subsquares) each 5 degrees lon, 2.5 degrees lat + grid_lat_subsq = chr(97 + int(adj_lat_remainder / 2.5)) + grid_lon_subsq = chr(97 + int(adj_lon_remainder / 5)) + + # remainder in seconds + adj_lat_remainder = (adj_lat_remainder % 2.5) * 60 + adj_lon_remainder = (adj_lon_remainder % 5.0) * 60 + + # divide into 10 zones (extended squares) each 30 secs lon, 15 secs lat + grid_lat_extsq = chr(48 + int(adj_lat_remainder / 15)) + grid_lon_extsq = chr(48 + int(adj_lon_remainder / 30)) + + return (grid_lon_sq + grid_lat_sq + + grid_lon_field + grid_lat_field + + grid_lon_subsq + grid_lat_subsq + + grid_lon_extsq + grid_lat_extsq) + +def __bilinear(lat, lon, table): + """Return bilinear interpolated data from table""" + + try: + lat = float(lat) + lon = float(lon) + except ValueError: + return '' + if _non_finite(lat) or _non_finite(lon): + return '' + + if math.fabs(lat) > 90 or math.fabs(lon) > 180: + return '' + + row = int(math.floor((90.0 + lat) / TABLE_SPAN)) + column = int(math.floor((180.0 + lon) / TABLE_SPAN)) + + if row < (TABLE_ROWS - 1): + grid_w = row + grid_e = row + 1 + else: + grid_w = row - 1 + grid_e = row + if column < (TABLE_COLS - 1): + grid_s = column + grid_n = column + 1 + else: + grid_s = column - 1 + grid_n = column + + south = grid_s * TABLE_SPAN - 180 + north = grid_n * TABLE_SPAN - 180 + west = grid_w * TABLE_SPAN - 90 + east = grid_e * TABLE_SPAN - 90 + + delta = TABLE_SPAN * TABLE_SPAN * 100 + from_west = lat - west + from_south = lon - south + from_east = east - lat + from_north = north - lon + + result = table[grid_e][grid_n] * from_west * from_south + result += table[grid_w][grid_n] * from_east * from_south + result += table[grid_e][grid_s] * from_west * from_north + result += table[grid_w][grid_s] * from_east * from_north + return result / delta + + +def mag_var(lat, lon): + """Return magnetic variation (declination) in degrees. +Given a lat/lon in degrees""" + return __bilinear(lat, lon, magvar_table) + + +def wgs84_separation(lat, lon): + """Return MSL-WGS84 geodetic separation in meters. +Given a lat/lon in degrees""" + return __bilinear(lat, lon, GEOID_DELTA) + +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/fake.py b/tools/gps/fake.py new file mode 100644 index 00000000..1d49f5b8 --- /dev/null +++ b/tools/gps/fake.py @@ -0,0 +1,843 @@ +# This code run compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +# +# This file is Copyright 2010 by the GPSD project +# SPDX-License-Identifier: BSD-2-Clause +""" +gpsfake.py -- classes for creating a controlled test environment around gpsd. + +The gpsfake(1) regression tester shipped with GPSD is a trivial wrapper +around this code. For a more interesting usage example, see the +valgrind-audit script shipped with the GPSD code. + +To use this code, start by instantiating a TestSession class. Use the +prefix argument if you want to run the daemon under some kind of run-time +monitor like valgrind or gdb. Here are some particularly useful possibilities: + +valgrind --tool=memcheck --gen-suppressions=yes --leak-check=yes + Run under Valgrind, checking for malloc errors and memory leaks. + +xterm -e gdb -tui --args + Run under gdb, controlled from a new xterm. + +You can use the options argument to pass in daemon options; normally you will +use this to set the debug-logging level. + +On initialization, the test object spawns an instance of gpsd with no +devices or clients attached, connected to a control socket. + +TestSession has methods to attach and detch fake GPSes. The +TestSession class simulates GPS devices for you with objects composed +from a pty and a class instance that cycles sentences into the master side +from some specified logfile; gpsd reads the slave side. A fake GPS is +identified by the string naming its slave device. + +TestSession also has methods to start and end client sessions. Daemon +responses to a client are fed to a hook function which, by default, +discards them. Note that this data is 'bytes' to accommodate possible +binary data in Python 3; use polystr() if you need a str. You can +change the hook to misc.get_bytes_stream(sys.stdout).write to dump +responses to standard output (this is what the gpsfake executable does) +or do something more exotic. A client session is identified by a small +integer that counts the number of client session starts. + +There are a couple of convenience methods. TestSession.wait() does nothing, +allowing a specified number of seconds to elapse. TestSession.send() +ships commands to an open client session. + +TestSession does not currently capture the daemon's log output. It is +run with -N, so the output will go to stderr (along with, for example, +Valgrind notifications). + +Each FakeGPS instance tries to packetize the data from the logfile it +is initialized with. It uses the same packet-getter as the daemon. +Exception: if there is a Delay-Cookie line in a header comment, that +delimiter is used to split up the test load. + +The TestSession code maintains a run queue of FakeGPS and gps.gs +(client- session) objects. It repeatedly cycles through the run queue. +For each client session object in the queue, it tries to read data +from gpsd. For each fake GPS, it sends one line or packet of stored +data. When a fake-GPS's go predicate becomes false, the fake GPS is +removed from the run queue. + +There are two ways to use this code. The more deterministic is +non-threaded mode: set up your client sessions and fake GPS devices, +then call the run() method. The run() method will terminate when +there are no more objects in the run queue. Note, you must have +created at least one fake client or fake GPS before calling run(), +otherwise it will terminate immediately. + +To allow for adding and removing clients while the test is running, +run in threaded mode by calling the start() method. This simply calls +the run method in a subthread, with locking of critical regions. +""" +# This code runs compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +from __future__ import absolute_import, print_function, division + +import os +import pty +import select +import signal +import socket +import stat +import subprocess +import sys +import termios # fcntl, array, struct +import threading +import time + +import gps +from . import packet as sniffer + +# The magic number below has to be derived from observation. If +# it's too high you'll slow the tests down a lot. If it's too low +# you'll get regression tests timing out. + +# WRITE_PAD: Define a per-line delay on writes so we won't spam the +# buffers in the pty layer or gpsd itself. Values smaller than the +# system timer tick don't make any difference here. Can be set from +# WRITE_PAD in the environment. + +if sys.platform.startswith("linux"): + WRITE_PAD = 0.0 +elif sys.platform.startswith("freebsd"): + WRITE_PAD = 0.01 +elif sys.platform.startswith("netbsd5"): + WRITE_PAD = 0.200 +elif sys.platform.startswith("netbsd"): + WRITE_PAD = 0.01 +elif sys.platform.startswith("darwin"): + # darwin Darwin-13.4.0-x86_64-i386-64bit + WRITE_PAD = 0.005 +else: + WRITE_PAD = 0.004 + +# Additional delays in slow mode +WRITE_PAD_SLOWDOWN = 0.01 + +# If a test takes longer than this, we deem it to have timed out +TEST_TIMEOUT = 60 + + +def GetDelay(slow=False): + "Get appropriate per-line delay." + delay = float(os.getenv("WRITE_PAD", WRITE_PAD)) + if slow: + delay += WRITE_PAD_SLOWDOWN + return delay + + +class TestError(BaseException): + "Class TestError" + def __init__(self, msg): + super(TestError, self).__init__() + self.msg = msg + + +class TestLoadError(TestError): + "Class TestLoadError, empty" + + +class TestLoad(object): + "Digest a logfile into a list of sentences we can cycle through." + + def __init__(self, logfp, predump=False, slow=False, oneshot=False): + self.sentences = [] # This is the interesting part + if isinstance(logfp, str): + logfp = open(logfp, "rb") + self.name = logfp.name + self.logfp = logfp + self.predump = predump + self.type = None + self.sourcetype = "pty" + self.serial = None + self.delay = GetDelay(slow) + self.delimiter = None + # Stash away a copy in case we need to resplit + text = logfp.read() + logfp = open(logfp.name, 'rb') + # Grab the packets in the normal way + getter = sniffer.new() + # gps.packet.register_report(reporter) + type_latch = None + commentlen = 0 + while True: + # Note that packet data is bytes rather than str + (plen, ptype, packet, _counter) = getter.get(logfp.fileno()) + if plen <= 0: + break + elif ptype == sniffer.COMMENT_PACKET: + commentlen += len(packet) + # Some comments are magic + if b"Serial:" in packet: + # Change serial parameters + packet = packet[1:].strip() + try: + (_xx, baud, params) = packet.split() + baud = int(baud) + if params[0] in (b'7', b'8'): + databits = int(params[0]) + else: + raise ValueError + if params[1] in (b'N', b'O', b'E'): + parity = params[1] + else: + raise ValueError + if params[2] in (b'1', b'2'): + stopbits = int(params[2]) + else: + raise ValueError + except (ValueError, IndexError): + raise TestLoadError("bad serial-parameter spec in %s" % + self.name) + self.serial = (baud, databits, parity, stopbits) + elif b"Transport: UDP" in packet: + self.sourcetype = "UDP" + elif b"Transport: TCP" in packet: + self.sourcetype = "TCP" + elif b"Delay-Cookie:" in packet: + if packet.startswith(b"#"): + packet = packet[1:] + try: + (_dummy, self.delimiter, delay) = \ + packet.strip().split() + self.delay = float(delay) + except ValueError: + raise TestLoadError("bad Delay-Cookie line in %s" % + self.name) + self.resplit = True + else: + if type_latch is None: + type_latch = ptype + if self.predump: + print(repr(packet)) + if not packet: + raise TestLoadError("zero-length packet from %s" % + self.name) + self.sentences.append(packet) + # Look at the first packet to grok the GPS type + self.textual = (type_latch == sniffer.NMEA_PACKET) + if self.textual: + self.legend = "gpsfake: line %d: " + else: + self.legend = "gpsfake: packet %d" + # Maybe this needs to be split on different delimiters? + if self.delimiter is not None: + self.sentences = text[commentlen:].split(self.delimiter) + # Do we want single-shot operation? + if oneshot: + self.sentences.append(b"# EOF\n") + + +class PacketError(TestError): + "Class PacketError, empty" + + +class FakeGPS(object): + "Class FakeGPS" + def __init__(self, testload, progress=lambda x: None): + self.exhausted = 0 + self.go_predicate = lambda: True + self.index = 0 + self.progress = progress + self.readers = 0 + self.testload = testload + self.progress("gpsfake: %s provides %d sentences\n" + % (self.testload.name, len(self.testload.sentences))) + + def write(self, line): + "Throw an error if this superclass is ever instantiated." + raise ValueError(line) + + def feed(self): + "Feed a line from the contents of the GPS log to the daemon." + line = self.testload.sentences[self.index + % len(self.testload.sentences)] + if b"%Delay:" in line: + # Delay specified number of seconds + delay = line.split()[1] + time.sleep(int(delay)) + # self.write has to be set by the derived class + self.write(line) + time.sleep(self.testload.delay) + self.index += 1 + + +class FakePTY(FakeGPS): + "A FakePTY is a pty with a test log ready to be cycled to it." + + def __init__(self, testload, + speed=4800, databits=8, parity='N', stopbits=1, + progress=lambda x: None): + super(FakePTY, self).__init__(testload, progress) + # Allow Serial: header to be overridden by explicit speed. + if self.testload.serial: + (speed, databits, parity, stopbits) = self.testload.serial + self.speed = speed + baudrates = { + 0: termios.B0, + 50: termios.B50, + 75: termios.B75, + 110: termios.B110, + 134: termios.B134, + 150: termios.B150, + 200: termios.B200, + 300: termios.B300, + 600: termios.B600, + 1200: termios.B1200, + 1800: termios.B1800, + 2400: termios.B2400, + 4800: termios.B4800, + 9600: termios.B9600, + 19200: termios.B19200, + 38400: termios.B38400, + 57600: termios.B57600, + 115200: termios.B115200, + 230400: termios.B230400, + } + (self.fd, self.slave_fd) = pty.openpty() + self.byname = os.ttyname(self.slave_fd) + os.chmod(self.byname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | + stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) + (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr( + self.slave_fd) + cc[termios.VMIN] = 1 + cflag &= ~(termios.PARENB | termios.PARODD | termios.CRTSCTS) + cflag |= termios.CREAD | termios.CLOCAL + iflag = oflag = lflag = 0 + iflag &= ~ (termios.PARMRK | termios.INPCK) + cflag &= ~ (termios.CSIZE | termios.CSTOPB | termios.PARENB | + termios.PARODD) + if databits == 7: + cflag |= termios.CS7 + else: + cflag |= termios.CS8 + if stopbits == 2: + cflag |= termios.CSTOPB + # Warning: attempting to set parity makes Fedora lose its cookies + if parity == 'E': + iflag |= termios.INPCK + cflag |= termios.PARENB + elif parity == 'O': + iflag |= termios.INPCK + cflag |= termios.PARENB | termios.PARODD + ispeed = ospeed = baudrates[speed] + try: + termios.tcsetattr(self.slave_fd, termios.TCSANOW, + [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) + except termios.error: + raise TestLoadError("error attempting to set serial mode to %s " + " %s%s%s" + % (speed, databits, parity, stopbits)) + + def read(self): + "Discard control strings written by gpsd." + # A tcflush implementation works on Linux but fails on OpenBSD 4. + termios.tcflush(self.fd, termios.TCIFLUSH) + # Alas, the FIONREAD version also works on Linux and fails on OpenBSD. + # try: + # buf = array.array('i', [0]) + # fcntl.ioctl(self.master_fd, termios.FIONREAD, buf, True) + # n = struct.unpack('i', buf)[0] + # os.read(self.master_fd, n) + # except IOError: + # pass + + def write(self, line): + self.progress("gpsfake: %s writes %d=%s\n" + % (self.testload.name, len(line), repr(line))) + os.write(self.fd, line) + + def drain(self): + "Wait for the associated device to drain (e.g. before closing)." + termios.tcdrain(self.fd) + + +def cleansocket(host, port, socktype=socket.SOCK_STREAM): + "Get a socket that we can re-use cleanly after it's closed." + cs = socket.socket(socket.AF_INET, socktype) + # This magic prevents "Address already in use" errors after + # we release the socket. + cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + cs.bind((host, port)) + return cs + + +def freeport(socktype=socket.SOCK_STREAM): + """Get a free port number for the given connection type. + + This lets the OS assign a unique port, and then assumes + that it will become available for reuse once the socket + is closed, and remain so long enough for the real use. + """ + s = cleansocket("127.0.0.1", 0, socktype) + port = s.getsockname()[1] + s.close() + return port + + +class FakeTCP(FakeGPS): + "A TCP serverlet with a test log ready to be cycled to it." + + def __init__(self, testload, + host, port, + progress=lambda x: None): + super(FakeTCP, self).__init__(testload, progress) + self.host = host + self.dispatcher = cleansocket(self.host, int(port)) + # Get actual assigned port + self.port = self.dispatcher.getsockname()[1] + self.byname = "tcp://" + host + ":" + str(self.port) + self.dispatcher.listen(5) + self.readables = [self.dispatcher] + + def read(self): + "Handle connection requests and data." + readable, _writable, _errored = select.select(self.readables, [], [], + 0) + for s in readable: + if s == self.dispatcher: # Connection request + client_socket, _address = s.accept() + self.readables = [client_socket] + # Depending on timing, gpsd may try to reconnect between the + # end of the log data and the remove_device. With no listener, + # this results in spurious error messages. Keeping the + # listener around avoids this. It will eventually be closed + # by the Python object cleanup. self.dispatcher.close() + else: # Incoming data + data = s.recv(1024) + if not data: + s.close() + self.readables.remove(s) + + def write(self, line): + "Send the next log packet to everybody connected." + self.progress("gpsfake: %s writes %d=%s\n" + % (self.testload.name, len(line), repr(line))) + for s in self.readables: + if s != self.dispatcher: + s.send(line) + + def drain(self): + "Wait for the associated device(s) to drain (e.g. before closing)." + for s in self.readables: + if s != self.dispatcher: + s.shutdown(socket.SHUT_RDWR) + + +class FakeUDP(FakeGPS): + "A UDP broadcaster with a test log ready to be cycled to it." + + def __init__(self, testload, + ipaddr, port, + progress=lambda x: None): + super(FakeUDP, self).__init__(testload, progress) + self.byname = "udp://" + ipaddr + ":" + str(port) + self.ipaddr = ipaddr + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def read(self): + "Discard control strings written by gpsd." + return + + def write(self, line): + self.progress("gpsfake: %s writes %d=%s\n" + % (self.testload.name, len(line), repr(line))) + self.sock.sendto(line, (self.ipaddr, int(self.port))) + + def drain(self): + "Wait for the associated device to drain (e.g. before closing)." + # shutdown() fails on UDP + return # shutdown() fails on UDP + + +class SubprogramError(TestError): + "Class SubprogramError" + def __str__(self): + return repr(self.msg) + + +class SubprogramInstance(object): + "Class for generic subprogram." + ERROR = SubprogramError + + def __init__(self): + self.spawncmd = None + self.process = None + self.returncode = None + self.env = None + + def spawn_sub(self, program, options, background=False, prefix="", + env=None): + "Spawn a subprogram instance." + spawncmd = None + + # Look for program in GPSD_HOME env variable + if os.environ.get('GPSD_HOME'): + for path in os.environ['GPSD_HOME'].split(':'): + _spawncmd = "%s/%s" % (path, program) + if os.path.isfile(_spawncmd) and os.access(_spawncmd, os.X_OK): + spawncmd = _spawncmd + break + + # if we could not find it yet try PATH env variable for it + if not spawncmd: + if '/usr/sbin' not in os.environ['PATH']: + os.environ['PATH'] = os.environ['PATH'] + ":/usr/sbin" + for path in os.environ['PATH'].split(':'): + _spawncmd = "%s/%s" % (path, program) + if os.path.isfile(_spawncmd) and os.access(_spawncmd, os.X_OK): + spawncmd = _spawncmd + break + + if not spawncmd: + raise self.ERROR("Cannot execute %s: executable not found. " + "Set GPSD_HOME env variable" % program) + self.spawncmd = [spawncmd] + options.split() + if prefix: + self.spawncmd = prefix.split() + self.spawncmd + if env: + self.env = os.environ.copy() + self.env.update(env) + self.process = subprocess.Popen(self.spawncmd, env=self.env) + if not background: + self.returncode = status = self.process.wait() + if os.WIFSIGNALED(status) or os.WEXITSTATUS(status): + raise self.ERROR("%s exited with status %d" + % (program, status)) + + def is_alive(self): + "Is the program still alive?" + if not self.process: + return False + self.returncode = self.process.poll() + if self.returncode is None: + return True + self.process = None + return False + + def kill(self): + "Kill the program instance." + while self.is_alive(): + try: # terminate() may fail if already killed + self.process.terminate() + except OSError: + continue + time.sleep(0.01) + + +class DaemonError(SubprogramError): + "Class DaemonError" + + +class DaemonInstance(SubprogramInstance): + "Control a gpsd instance." + ERROR = DaemonError + + def __init__(self, control_socket=None): + self.sock = None + super(DaemonInstance, self).__init__() + if control_socket: + self.control_socket = control_socket + else: + tmpdir = os.environ.get('TMPDIR', '/tmp') + self.control_socket = "%s/gpsfake-%d.sock" % (tmpdir, os.getpid()) + + def spawn(self, options, port, background=False, prefix=""): + "Spawn a daemon instance." + # The -b option to suppress hanging on probe returns is needed to cope + # with OpenBSD (and possibly other non-Linux systems) that don't + # support anything we can use to implement the FakeGPS.read() method + opts = (" -b -N -S %s -F %s %s" + % (port, self.control_socket, options)) + # Derive a unique SHM key from the port # to avoid collisions. + # Use 'Gp' as the prefix to avoid colliding with 'GPSD'. + shmkey = '0x4770%.04X' % int(port) + env = {'GPSD_SHM_KEY': shmkey} + self.spawn_sub('gpsd', opts, background, prefix, env) + + def wait_ready(self): + "Wait for the daemon to create the control socket." + while self.is_alive(): + if os.path.exists(self.control_socket): + return + time.sleep(0.1) + + def __get_control_socket(self): + # Now we know it's running, get a connection to the control socket. + if not os.path.exists(self.control_socket): + return None + try: + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) + self.sock.connect(self.control_socket) + except socket.error: + if self.sock: + self.sock.close() + self.sock = None + return self.sock + + def add_device(self, path): + "Add a device to the daemon's internal search list." + if self.__get_control_socket(): + self.sock.sendall(gps.polybytes("+%s\r\n\x00" % path)) + self.sock.recv(12) + self.sock.close() + + def remove_device(self, path): + "Remove a device from the daemon's internal search list." + if self.__get_control_socket(): + self.sock.sendall(gps.polybytes("-%s\r\n\x00" % path)) + self.sock.recv(12) + self.sock.close() + + +class TestSessionError(TestError): + "class TestSessionError" + # why does testSessionError() do nothing? " + + +class TestSession(object): + "Manage a session including a daemon with fake GPSes and clients." + + def __init__(self, prefix=None, port=None, options=None, verbose=0, + predump=False, udp=False, tcp=False, slow=False, + timeout=None): + "Initialize the test session by launching the daemon." + self.prefix = prefix + self.options = options + self.verbose = verbose + self.predump = predump + self.udp = udp + self.tcp = tcp + self.slow = slow + self.daemon = DaemonInstance() + self.fakegpslist = {} + self.client_id = 0 + self.readers = 0 + self.writers = 0 + self.runqueue = [] + self.index = 0 + if port: + self.port = port + else: + self.port = freeport() + self.progress = lambda x: None + # for debugging + # self.progress = lambda x: sys.stderr.write("# Hi " + x) + self.reporter = lambda x: None + self.default_predicate = None + self.fd_set = [] + self.threadlock = None + self.timeout = TEST_TIMEOUT if timeout is None else timeout + + def spawn(self): + "Spawn daemon" + for sig in (signal.SIGQUIT, signal.SIGINT, signal.SIGTERM): + signal.signal(sig, lambda unused, dummy: self.cleanup()) + self.daemon.spawn(background=True, prefix=self.prefix, port=self.port, + options=self.options) + self.daemon.wait_ready() + + def set_predicate(self, pred): + "Set a default go predicate for the session." + self.default_predicate = pred + + def gps_add(self, logfile, speed=19200, pred=None, oneshot=False): + "Add a simulated GPS being fed by the specified logfile." + self.progress("gpsfake: gps_add(%s, %d)\n" % (logfile, speed)) + if logfile not in self.fakegpslist: + testload = TestLoad(logfile, predump=self.predump, slow=self.slow, + oneshot=oneshot) + if testload.sourcetype == "UDP" or self.udp: + newgps = FakeUDP(testload, ipaddr="127.0.0.1", + port=freeport(socket.SOCK_DGRAM), + progress=self.progress) + elif testload.sourcetype == "TCP" or self.tcp: + # Let OS assign the port + newgps = FakeTCP(testload, host="127.0.0.1", port=0, + progress=self.progress) + else: + newgps = FakePTY(testload, speed=speed, + progress=self.progress) + if pred: + newgps.go_predicate = pred + elif self.default_predicate: + newgps.go_predicate = self.default_predicate + self.fakegpslist[newgps.byname] = newgps + self.append(newgps) + newgps.exhausted = 0 + self.daemon.add_device(newgps.byname) + return newgps.byname + + def gps_remove(self, name): + "Remove a simulated GPS from the daemon's search list." + self.progress("gpsfake: gps_remove(%s)\n" % name) + self.fakegpslist[name].drain() + self.remove(self.fakegpslist[name]) + self.daemon.remove_device(name) + del self.fakegpslist[name] + + def client_add(self, commands): + "Initiate a client session and force connection to a fake GPS." + self.progress("gpsfake: client_add()\n") + try: + newclient = gps.gps(port=self.port, verbose=self.verbose) + except socket.error: + if not self.daemon.is_alive(): + raise TestSessionError("daemon died") + raise + self.append(newclient) + newclient.id = self.client_id + 1 + self.client_id += 1 + self.progress("gpsfake: client %d has %s\n" + % (self.client_id, newclient.device)) + if commands: + self.initialize(newclient, commands) + return self.client_id + + def client_remove(self, cid): + "Terminate a client session." + self.progress("gpsfake: client_remove(%d)\n" % cid) + for obj in self.runqueue: + if isinstance(obj, gps.gps) and obj.id == cid: + self.remove(obj) + return True + return False + + def wait(self, seconds): + "Wait, doing nothing." + self.progress("gpsfake: wait(%d)\n" % seconds) + time.sleep(seconds) + + def gather(self, seconds): + "Wait, doing nothing but watching for sentences." + self.progress("gpsfake: gather(%d)\n" % seconds) + time.sleep(seconds) + + def cleanup(self): + "We're done, kill the daemon." + self.progress("gpsfake: cleanup()\n") + if self.daemon: + self.daemon.kill() + self.daemon = None + + def run(self): + "Run the tests." + try: + self.progress("gpsfake: test loop begins\n") + while self.daemon: + if not self.daemon.is_alive(): + raise TestSessionError("daemon died") + # We have to read anything that gpsd might have tried + # to send to the GPS here -- under OpenBSD the + # TIOCDRAIN will hang, otherwise. + for device in self.runqueue: + if isinstance(device, FakeGPS): + device.read() + had_output = False + chosen = self.choose() + if isinstance(chosen, FakeGPS): + if (((chosen.exhausted and self.timeout and + (time.time() - chosen.exhausted > self.timeout) and + chosen.byname in self.fakegpslist))): + sys.stderr.write( + "Test timed out: maybe increase WRITE_PAD (= %s)\n" + % GetDelay(self.slow)) + raise SystemExit(1) + + if not chosen.go_predicate(chosen.index, chosen): + if chosen.exhausted == 0: + chosen.exhausted = time.time() + self.progress("gpsfake: GPS %s ran out of input\n" + % chosen.byname) + else: + chosen.feed() + elif isinstance(chosen, gps.gps): + if chosen.enqueued: + chosen.send(chosen.enqueued) + chosen.enqueued = "" + while chosen.waiting(): + if not self.daemon or not self.daemon.is_alive(): + raise TestSessionError("daemon died") + ret = chosen.read() + if 0 > ret: + raise TestSessionError("daemon output stopped") + # FIXME: test for 0 == ret. + had_output = True + if not chosen.valid & gps.PACKET_SET: + continue + self.reporter(chosen.bresponse) + if ((chosen.data["class"] == "DEVICE" and + chosen.data["activated"] == 0 and + chosen.data["path"] in self.fakegpslist)): + self.gps_remove(chosen.data["path"]) + self.progress( + "gpsfake: GPS %s removed (notification)\n" + % chosen.data["path"]) + else: + raise TestSessionError("test object of unknown type") + if not self.writers and not had_output: + self.progress("gpsfake: no writers and no output\n") + break + self.progress("gpsfake: test loop ends\n") + finally: + self.cleanup() + + # All knowledge about locks and threading is below this line, + # except for the bare fact that self.threadlock is set to None + # in the class init method. + + def append(self, obj): + "Add a producer or consumer to the object list." + if self.threadlock: + self.threadlock.acquire() + self.runqueue.append(obj) + if isinstance(obj, FakeGPS): + self.writers += 1 + elif isinstance(obj, gps.gps): + self.readers += 1 + if self.threadlock: + self.threadlock.release() + + def remove(self, obj): + "Remove a producer or consumer from the object list." + if self.threadlock: + self.threadlock.acquire() + self.runqueue.remove(obj) + if isinstance(obj, FakeGPS): + self.writers -= 1 + elif isinstance(obj, gps.gps): + self.readers -= 1 + self.index = min(len(self.runqueue) - 1, self.index) + if self.threadlock: + self.threadlock.release() + + def choose(self): + "Atomically get the next object scheduled to do something." + if self.threadlock: + self.threadlock.acquire() + chosen = self.index + self.index += 1 + self.index %= len(self.runqueue) + if self.threadlock: + self.threadlock.release() + return self.runqueue[chosen] + + def initialize(self, client, commands): + "Arrange for client to ship specified commands when it goes active." + client.enqueued = "" + if not self.threadlock: + client.send(commands) + else: + client.enqueued = commands + + def start(self): + "Start thread" + self.threadlock = threading.Lock() + threading.Thread(target=self.run) + +# End +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/gps.py b/tools/gps/gps.py new file mode 100755 index 00000000..a295aac9 --- /dev/null +++ b/tools/gps/gps.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# This code is generated by scons. Do not hand-hack it! +'''gps.py -- Python interface to GPSD. + +This interface has a lot of historical cruft in it related to old +protocol, and was modeled on the C interface. It won't be thrown +away, but it's likely to be deprecated in favor of something more +Pythonic. + +The JSON parts of this (which will be reused by any new interface) +now live in a different module. +''' + +# +# This file is Copyright 2010 by the GPSD project +# SPDX-License-Identifier: BSD-2-Clause +# + +# This code runs compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +from __future__ import absolute_import, print_function, division + +from .client import * +from .watch_options import * + + +NaN = float('nan') + + +def isfinite(f): + "Check if f is finite" + # Python 2 does not think +Inf or -Inf are NaN + # Python 2 has no easier way to test for Inf + return float('-inf') < float(f) < float('inf') + + +# Don't hand-hack this list, it's generated. +ONLINE_SET = (1 << 1) +TIME_SET = (1 << 2) +TIMERR_SET = (1 << 3) +LATLON_SET = (1 << 4) +ALTITUDE_SET = (1 << 5) +SPEED_SET = (1 << 6) +TRACK_SET = (1 << 7) +CLIMB_SET = (1 << 8) +STATUS_SET = (1 << 9) +MODE_SET = (1 << 10) +DOP_SET = (1 << 11) +HERR_SET = (1 << 12) +VERR_SET = (1 << 13) +ATTITUDE_SET = (1 << 14) +SATELLITE_SET = (1 << 15) +SPEEDERR_SET = (1 << 16) +TRACKERR_SET = (1 << 17) +CLIMBERR_SET = (1 << 18) +DEVICE_SET = (1 << 19) +DEVICELIST_SET = (1 << 20) +DEVICEID_SET = (1 << 21) +RTCM2_SET = (1 << 22) +RTCM3_SET = (1 << 23) +AIS_SET = (1 << 24) +PACKET_SET = (1 << 25) +SUBFRAME_SET = (1 << 26) +GST_SET = (1 << 27) +VERSION_SET = (1 << 28) +POLICY_SET = (1 << 29) +LOGMESSAGE_SET = (1 << 30) +ERROR_SET = (1 << 31) +TIMEDRIFT_SET = (1 << 32) +EOF_SET = (1 << 33) +SET_HIGH_BIT = 34 +UNION_SET = (RTCM2_SET | RTCM3_SET | SUBFRAME_SET | AIS_SET | VERSION_SET | + DEVICELIST_SET | ERROR_SET | GST_SET) +STATUS_NO_FIX = 0 +STATUS_FIX = 1 +STATUS_DGPS_FIX = 2 +STATUS_RTK_FIX = 3 +STATUS_RTK_FLT = 4 +STATUS_DR = 5 +STATUS_GNSSDR = 6 +STATUS_TIME = 7 +STATUS_SIM = 8 +STATUS_PPS_FIX = 9 +MODE_NO_FIX = 1 +MODE_2D = 2 +MODE_3D = 3 +MAXCHANNELS = 72 # Copied from gps.h, but not required to match +SIGNAL_STRENGTH_UNKNOWN = NaN + + +class gpsfix(object): + "Class to hold one GPS fix" + + def __init__(self): + "Init class gpsfix" + + self.altitude = NaN # Meters DEPRECATED + self.altHAE = NaN # Meters + self.altMSL = NaN # Meters + self.climb = NaN # Meters per second + self.datum = "" + self.dgpsAge = -1 + self.dgpsSta = "" + self.depth = NaN + self.device = "" + self.ecefx = NaN + self.ecefy = NaN + self.ecefz = NaN + self.ecefvx = NaN + self.ecefvy = NaN + self.ecefvz = NaN + self.ecefpAcc = NaN + self.ecefvAcc = NaN + self.epc = NaN + self.epd = NaN + self.eph = NaN + self.eps = NaN + self.ept = NaN + self.epv = NaN + self.epx = NaN + self.epy = NaN + self.geoidSep = NaN # Meters + self.latitude = self.longitude = 0.0 + self.magtrack = NaN + self.magvar = NaN + self.mode = MODE_NO_FIX + self.relN = NaN + self.relE = NaN + self.relD = NaN + self.sep = NaN # a.k.a. epe + self.speed = NaN # Knots + self.status = STATUS_NO_FIX + self.time = NaN + self.track = NaN # Degrees from true north + self.velN = NaN + self.velE = NaN + self.velD = NaN + + +class gpsdata(object): + "Position, track, velocity and status information returned by a GPS." + + class satellite(object): + "Class to hold satellite data" + def __init__(self, PRN, elevation, azimuth, ss, used=None): + self.PRN = PRN + self.elevation = elevation + self.azimuth = azimuth + self.ss = ss + self.used = used + + def __repr__(self): + return "PRN: %3d E: %3d Az: %3d Ss: %3d Used: %s" % ( + self.PRN, self.elevation, self.azimuth, self.ss, + "ny"[self.used]) + + def __init__(self): + # Initialize all data members + self.online = 0 # NZ if GPS on, zero if not + + self.valid = 0 + self.fix = gpsfix() + + self.status = STATUS_NO_FIX + self.utc = "" + + self.satellites_used = 0 # Satellites used in last fix + self.xdop = self.ydop = self.vdop = self.tdop = 0 + self.pdop = self.hdop = self.gdop = 0.0 + + self.epe = 0.0 + + self.satellites = [] # satellite objects in view + + self.gps_id = None + self.driver_mode = 0 + self.baudrate = 0 + self.stopbits = 0 + self.cycle = 0 + self.mincycle = 0 + self.device = None + self.devices = [] + + self.version = None + + def __repr__(self): + st = "Time: %s (%s)\n" % (self.utc, self.fix.time) + st += "Lat/Lon: %f %f\n" % (self.fix.latitude, self.fix.longitude) + if not isfinite(self.fix.altHAE): + st += "Altitude HAE: ?\n" + else: + st += "Altitude HAE: %f\n" % (self.fix.altHAE) + if not isfinite(self.fix.speed): + st += "Speed: ?\n" + else: + st += "Speed: %f\n" % (self.fix.speed) + if not isfinite(self.fix.track): + st += "Track: ?\n" + else: + st += "Track: %f\n" % (self.fix.track) + st += "Status: STATUS_%s\n" \ + % ("NO_FIX", "FIX", "DGPS_FIX")[self.status] + st += "Mode: MODE_%s\n" \ + % ("ZERO", "NO_FIX", "2D", "3D")[self.fix.mode] + st += "Quality: %d p=%2.2f h=%2.2f v=%2.2f t=%2.2f g=%2.2f\n" % \ + (self.satellites_used, self.pdop, self.hdop, self.vdop, + self.tdop, self.gdop) + st += "Y: %s satellites in view:\n" % len(self.satellites) + for sat in self.satellites: + st += " %r\n" % sat + return st + + +class gps(gpscommon, gpsdata, gpsjson): + "Client interface to a running gpsd instance." + + # module version, would be nice to automate the version + __version__ = "3.20.1~dev" + + def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0, mode=0, + reconnect=False): + self.activated = None + self.clock_sec = NaN + self.clock_nsec = NaN + self.path = '' + self.precision = 0 + self.real_sec = NaN + self.real_nsec = NaN + self.serialmode = "8N1" + gpscommon.__init__(self, host, port, verbose, reconnect) + gpsdata.__init__(self) + gpsjson.__init__(self) + if mode: + self.stream(mode) + + def _oldstyle_shim(self): + # The rest is backwards compatibility for the old interface + def default(k, dflt, vbit=0): + "Return default for key" + if k not in self.data.keys(): + return dflt + + self.valid |= vbit + return self.data[k] + + if self.data.get("class") == "VERSION": + self.version = self.data + elif self.data.get("class") == "DEVICE": + self.valid = ONLINE_SET | DEVICE_SET + self.path = self.data["path"] + self.activated = default("activated", None) + driver = default("driver", None, DEVICEID_SET) + subtype = default("subtype", None, DEVICEID_SET) + self.gps_id = driver + if subtype: + self.gps_id += " " + subtype + self.baudrate = default("bps", 0) + self.cycle = default("cycle", NaN) + self.driver_mode = default("native", 0) + self.mincycle = default("mincycle", NaN) + self.serialmode = default("serialmode", "8N1") + elif self.data.get("class") == "TPV": + self.device = default("device", "missing") + self.utc = default("time", None, TIME_SET) + self.valid = ONLINE_SET + if self.utc is not None: + # self.utc is always iso 8601 string + # just copy to fix.time + self.fix.time = self.utc + self.fix.altitude = default("alt", NaN, ALTITUDE_SET) # DEPRECATED + self.fix.altHAE = default("altHAE", NaN, ALTITUDE_SET) + self.fix.altMSL = default("altMSL", NaN, ALTITUDE_SET) + self.fix.climb = default("climb", NaN, CLIMB_SET) + self.fix.epc = default("epc", NaN, CLIMBERR_SET) + self.fix.epd = default("epd", NaN) + self.fix.eps = default("eps", NaN, SPEEDERR_SET) + self.fix.ept = default("ept", NaN, TIMERR_SET) + self.fix.epv = default("epv", NaN, VERR_SET) + self.fix.epx = default("epx", NaN, HERR_SET) + self.fix.epy = default("epy", NaN, HERR_SET) + self.fix.latitude = default("lat", NaN, LATLON_SET) + self.fix.longitude = default("lon", NaN) + self.fix.mode = default("mode", 0, MODE_SET) + self.fix.speed = default("speed", NaN, SPEED_SET) + self.fix.status = default("status", 1) + self.fix.track = default("track", NaN, TRACK_SET) + elif self.data.get("class") == "SKY": + self.device = default("device", "missing") + for attrp in ("g", "h", "p", "t", "v", "x", "y"): + n = attrp + "dop" + setattr(self, n, default(n, NaN, DOP_SET)) + if "satellites" in self.data.keys(): + self.satellites = [] + for sat in self.data['satellites']: + if 'el' not in sat: + sat['el'] = -999 + if 'az' not in sat: + sat['az'] = -999 + if 'ss' not in sat: + sat['ss'] = -999 + self.satellites.append(gps.satellite(PRN=sat['PRN'], + elevation=sat['el'], + azimuth=sat['az'], ss=sat['ss'], + used=sat['used'])) + self.satellites_used = 0 + for sat in self.satellites: + if sat.used: + self.satellites_used += 1 + self.valid = ONLINE_SET | SATELLITE_SET + elif self.data.get("class") == "PPS": + self.device = default("device", "missing") + self.real_sec = default("real_sec", NaN) + self.real_nsec = default("real_nsec", NaN) + self.clock_sec = default("clock_sec", NaN) + self.clock_nsec = default("clock_nsec", NaN) + self.precision = default("precision", 0) + # elif self.data.get("class") == "DEVICES": + # TODO: handle class DEVICES # pylint: disable=fixme + + def read(self): + "Read and interpret data from the daemon." + status = gpscommon.read(self) + if status <= 0: + return status + if self.response.startswith("{") and self.response.endswith("}\r\n"): + self.unpack(self.response) + self._oldstyle_shim() + self.valid |= PACKET_SET + return 0 + + def __next__(self): + "Python 3 version of next()." + if self.read() == -1: + raise StopIteration + if hasattr(self, "data"): + return self.data + + return self.response + + def next(self): + "Python 2 backward compatibility." + return self.__next__() + + def stream(self, flags=0, devpath=None): + "Ask gpsd to stream reports at your client." + + gpsjson.stream(self, flags, devpath) + + +def is_sbas(prn): + "Is this the NMEA ID of an SBAS satellite?" + return 120 <= prn <= 158 + + +if __name__ == '__main__': + import getopt + import sys + (options, arguments) = getopt.getopt(sys.argv[1:], "v") + streaming = False + verbose = False + for (switch, val) in options: + if switch == '-v': + verbose = True + if len(arguments) > 2: + print('Usage: gps.py [-v] [host [port]]') + sys.exit(1) + + opts = {"verbose": verbose} + if arguments: + opts["host"] = arguments[0] + if arguments: + opts["port"] = arguments[1] + + session = gps(**opts) + session.stream(WATCH_ENABLE) + try: + for report in session: + print(report) + except KeyboardInterrupt: + # Avoid garble on ^C + print("") + +# gps.py ends here +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/misc.py b/tools/gps/misc.py new file mode 100644 index 00000000..4e8fdab2 --- /dev/null +++ b/tools/gps/misc.py @@ -0,0 +1,294 @@ +# misc.py - miscellaneous geodesy and time functions +"miscellaneous geodesy and time functions" +# +# This file is Copyright 2010 by the GPSD project +# SPDX-License-Identifier: BSD-2-Clause + +# This code runs compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +from __future__ import absolute_import, print_function, division + +import calendar +import io +import math +import time + + +def monotonic(): + """return monotonic seconds, of unknown epoch. + Python 2 to 3.7 has time.clock(), deprecates in 3.3+, removed in 3.8 + Python 3.5+ has time.monotonic() + This always works + """ + + if hasattr(time, 'monotonic'): + return time.monotonic() + # else + return time.clock() + + +# Determine a single class for testing "stringness" +try: + STR_CLASS = basestring # Base class for 'str' and 'unicode' in Python 2 +except NameError: + STR_CLASS = str # In Python 3, 'str' is the base class + +# We need to be able to handle data which may be a mixture of text and binary +# data. The text in this context is known to be limited to US-ASCII, so +# there aren't any issues regarding character sets, but we need to ensure +# that binary data is preserved. In Python 2, this happens naturally with +# "strings" and the 'str' and 'bytes' types are synonyms. But in Python 3, +# these are distinct types (with 'str' being based on Unicode), and conversions +# are encoding-sensitive. The most straightforward encoding to use in this +# context is 'latin-1' (a.k.a.'iso-8859-1'), which directly maps all 256 +# 8-bit character values to Unicode page 0. Thus, if we can enforce the use +# of 'latin-1' encoding, we can preserve arbitrary binary data while correctly +# mapping any actual text to the proper characters. + +BINARY_ENCODING = 'latin-1' + +if bytes is str: # In Python 2 these functions can be null transformations + + polystr = str + polybytes = bytes + + def make_std_wrapper(stream): + "Dummy stdio wrapper function." + return stream + + def get_bytes_stream(stream): + "Dummy stdio bytes buffer function." + return stream + +else: # Otherwise we do something real + + def polystr(o): + "Convert bytes or str to str with proper encoding." + if isinstance(o, str): + return o + if isinstance(o, bytes) or isinstance(o, bytearray): + return str(o, encoding=BINARY_ENCODING) + if isinstance(o, int): + return str(o) + raise ValueError + + def polybytes(o): + "Convert bytes or str to bytes with proper encoding." + if isinstance(o, bytes): + return o + if isinstance(o, str): + return bytes(o, encoding=BINARY_ENCODING) + raise ValueError + + def make_std_wrapper(stream): + "Standard input/output wrapper factory function" + # This ensures that the encoding of standard output and standard + # error on Python 3 matches the binary encoding we use to turn + # bytes to Unicode in polystr above. + # + # newline="\n" ensures that Python 3 won't mangle line breaks + # line_buffering=True ensures that interactive command sessions + # work as expected + return io.TextIOWrapper(stream.buffer, encoding=BINARY_ENCODING, + newline="\n", line_buffering=True) + + def get_bytes_stream(stream): + "Standard input/output bytes buffer function" + return stream.buffer + + +# some multipliers for interpreting GPS output +# Note: A Texas Foot is ( meters * 3937/1200) +# (Texas Natural Resources Code, Subchapter D, Sec 21.071 - 79) +# not the same as an international fooot. +FEET_TO_METERS = 0.3048 # U.S./British feet to meters, exact +METERS_TO_FEET = (1 / FEET_TO_METERS) # Meters to U.S./British feet, exact +MILES_TO_METERS = 1.609344 # Miles to meters, exact +METERS_TO_MILES = (1 / MILES_TO_METERS) # Meters to miles, exact +FATHOMS_TO_METERS = 1.8288 # Fathoms to meters, exact +METERS_TO_FATHOMS = (1 / FATHOMS_TO_METERS) # Meters to fathoms, exact +KNOTS_TO_MPH = (1852 / 1609.344) # Knots to miles per hour, exact +KNOTS_TO_KPH = 1.852 # Knots to kilometers per hour, exact +MPS_TO_KPH = 3.6 # Meters per second to klicks/hr, exact +KNOTS_TO_MPS = (KNOTS_TO_KPH / MPS_TO_KPH) # Knots to meters per second, exact +MPS_TO_MPH = (1 / 0.44704) # Meters/second to miles per hour, exact +MPS_TO_KNOTS = (3600.0 / 1852.0) # Meters per second to knots, exact + + +def Deg2Rad(x): + "Degrees to radians." + return x * (math.pi / 180) + + +def Rad2Deg(x): + "Radians to degrees." + return x * (180 / math.pi) + + +def CalcRad(lat): + "Radius of curvature in meters at specified latitude WGS-84." + # the radius of curvature of an ellipsoidal Earth in the plane of a + # meridian of latitude is given by + # + # R' = a * (1 - e^2) / (1 - e^2 * (sin(lat))^2)^(3/2) + # + # where + # a is the equatorial radius (surface to center distance), + # b is the polar radius (surface to center distance), + # e is the first eccentricity of the ellipsoid + # e2 is e^2 = (a^2 - b^2) / a^2 + # es is the second eccentricity of the ellipsoid (UNUSED) + # es2 is es^2 = (a^2 - b^2) / b^2 + # + # for WGS-84: + # a = 6378.137 km (3963 mi) + # b = 6356.752314245 km (3950 mi) + # e2 = 0.00669437999014132 + # es2 = 0.00673949674227643 + a = 6378.137 + e2 = 0.00669437999014132 + sc = math.sin(math.radians(lat)) + x = a * (1.0 - e2) + z = 1.0 - e2 * pow(sc, 2) + y = pow(z, 1.5) + r = x / y + + r = r * 1000.0 # Convert to meters + return r + + +def EarthDistance(c1, c2): + """ + Vincenty's formula (inverse method) to calculate the distance (in + kilometers or miles) between two points on the surface of a spheroid + WGS 84 accurate to 1mm! + """ + + (lat1, lon1) = c1 + (lat2, lon2) = c2 + + # WGS 84 + a = 6378137 # meters + f = 1 / 298.257223563 + b = 6356752.314245 # meters; b = (1 - f)a + + # MILES_PER_KILOMETER = 1000.0 / (.3048 * 5280.0) + + MAX_ITERATIONS = 200 + CONVERGENCE_THRESHOLD = 1e-12 # .000,000,000,001 + + # short-circuit coincident points + if lat1 == lat2 and lon1 == lon2: + return 0.0 + + U1 = math.atan((1 - f) * math.tan(math.radians(lat1))) + U2 = math.atan((1 - f) * math.tan(math.radians(lat2))) + L = math.radians(lon1 - lon2) + Lambda = L + + sinU1 = math.sin(U1) + cosU1 = math.cos(U1) + sinU2 = math.sin(U2) + cosU2 = math.cos(U2) + + for _ in range(MAX_ITERATIONS): + sinLambda = math.sin(Lambda) + cosLambda = math.cos(Lambda) + sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 + + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2) + if sinSigma == 0: + return 0.0 # coincident points + cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda + sigma = math.atan2(sinSigma, cosSigma) + sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma + cosSqAlpha = 1 - sinAlpha ** 2 + try: + cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha + except ZeroDivisionError: + cos2SigmaM = 0 + C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)) + LambdaPrev = Lambda + Lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma * + (cos2SigmaM + C * cosSigma * + (-1 + 2 * cos2SigmaM ** 2))) + if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD: + break # successful convergence + else: + # failure to converge + # fall back top EarthDistanceSmall + return EarthDistanceSmall(c1, c2) + + uSq = cosSqAlpha * (a ** 2 - b ** 2) / (b ** 2) + A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) + B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) + deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * ( + cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * + (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2))) + s = b * A * (sigma - deltaSigma) + + # return meters to 6 decimal places + return round(s, 6) + + +def EarthDistanceSmall(c1, c2): + "Distance in meters between two close points specified in degrees." + # This calculation is known as an Equirectangular Projection + # fewer numeric issues for small angles that other methods + # the main use here is for when Vincenty's fails to converge. + (lat1, lon1) = c1 + (lat2, lon2) = c2 + avglat = (lat1 + lat2) / 2 + phi = math.radians(avglat) # radians of avg latitude + # meters per degree at this latitude, corrected for WGS84 ellipsoid + # Note the wikipedia numbers are NOT ellipsoid corrected: + # https://en.wikipedia.org/wiki/Decimal_degrees#Precision + m_per_d = (111132.954 - 559.822 * math.cos(2 * phi) + + 1.175 * math.cos(4 * phi)) + dlat = (lat1 - lat2) * m_per_d + dlon = (lon1 - lon2) * m_per_d * math.cos(phi) + + dist = math.sqrt(math.pow(dlat, 2) + math.pow(dlon, 2)) + return dist + + +def MeterOffset(c1, c2): + "Return offset in meters of second arg from first." + (lat1, lon1) = c1 + (lat2, lon2) = c2 + dx = EarthDistance((lat1, lon1), (lat1, lon2)) + dy = EarthDistance((lat1, lon1), (lat2, lon1)) + if lat1 < lat2: + dy = -dy + if lon1 < lon2: + dx = -dx + return (dx, dy) + + +def isotime(s): + "Convert timestamps in ISO8661 format to and from Unix time." + if isinstance(s, int): + return time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s)) + + if isinstance(s, float): + date = int(s) + msec = s - date + date = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s)) + return date + "." + repr(msec)[3:] + + if isinstance(s, STR_CLASS): + if s[-1] == "Z": + s = s[:-1] + if "." in s: + (date, msec) = s.split(".") + else: + date = s + msec = "0" + # Note: no leap-second correction! + return calendar.timegm( + time.strptime(date, "%Y-%m-%dT%H:%M:%S")) + float("0." + msec) + + # else: + raise TypeError + +# End +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/packet.py b/tools/gps/packet.py new file mode 100755 index 00000000..599a9a4f --- /dev/null +++ b/tools/gps/packet.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# +# This code is generated by scons. Do not hand-hack it! +# +# -*- coding: utf-8 -*- +# +# packet.py - helper functions for various bit +# +# theoretically comprised of reusable bits, in practice probably not so much. +# +# This code run compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +# +# This file is Copyright 2019 by the GPSD project +# SPDX-License-Identifier: BSD-2-Clause +"""Python binding of the libgpsd module for recognizing GPS packets. + +The new() function returns a new packet-lexer instance. Lexer instances +have two methods: + get() takes a file descriptor argument and returns a tuple consisting of +the integer packet type and string packet value. On end of file it returns +(-1, ''). + reset() resets the packet-lexer to its initial state. + The module also has a register_report() function that accepts a callback +for debug message reporting. The callback will get two arguments, the error +level of the message and the message itself. +""" +from __future__ import absolute_import, print_function +import ctypes +import gps.misc +import os + + +# Packet types and Logging levels extracted from gpsd.h +MAX_PACKET_LENGTH = 9216 +COMMENT_PACKET = 0 +NMEA_PACKET = 1 +AIVDM_PACKET = 2 +GARMINTXT_PACKET = 3 +SIRF_PACKET = 4 +ZODIAC_PACKET = 5 +TSIP_PACKET = 6 +EVERMORE_PACKET = 7 +ITALK_PACKET = 8 +GARMIN_PACKET = 9 +NAVCOM_PACKET = 10 +UBX_PACKET = 11 +SUPERSTAR2_PACKET = 12 +ONCORE_PACKET = 13 +GEOSTAR_PACKET = 14 +NMEA2000_PACKET = 15 +GREIS_PACKET = 16 +MAX_GPSPACKET_TYPE = 16 +RTCM2_PACKET = 17 +RTCM3_PACKET = 18 +JSON_PACKET = 19 +PACKET_TYPES = 20 +SKY_PACKET = 21 +LOG_SHOUT = 0 +LOG_WARN = 1 +LOG_CLIENT = 2 +LOG_INF = 3 +LOG_PROG = 4 +LOG_IO = 5 +LOG_DATA = 6 +LOG_SPIN = 7 +LOG_RAW = 8 +ISGPS_ERRLEVEL_BASE = LOG_RAW + + +class GpsdErrOutT(ctypes.Structure): + pass + + +# Add raw interface to the packet FFI stub. +_packet = None +_cwd = os.getcwd() +if 'gpsd' in _cwd.split('/'): + _path = _cwd + '/' +else: + _path = '/usr/local/lib' + if _path[-1] != '/': + _path += '/' + +try: + _packet = ctypes.CDLL('%slibgpsdpacket.so.27.0.0' % _path) +except OSError: + print('Failed to load the library:\t%slibgpsdpacket.so.27.0.0' % _path) + exit(1) + +_lexer_size = ctypes.c_size_t.in_dll(_packet, "fvi_size_lexer") +LEXER_SIZE = _lexer_size.value +_buffer_size = ctypes.c_size_t.in_dll(_packet, "fvi_size_buffer").value + +REPORTER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p) +GpsdErrOutT._fields_ = [('debug', ctypes.c_int), + ('report', REPORTER), + ('label', ctypes.c_char_p)] + + +class lexer_t(ctypes.Structure): + _fields_ = [ + ('packet_type', ctypes.c_int), + ('state', ctypes.c_uint), + ('length', ctypes.c_size_t), + ('inbuffer', ctypes.c_ubyte * _buffer_size), + ('inbuflen', ctypes.c_size_t), + ('inbufptr', ctypes.c_char_p), + ('outbuffer', ctypes.c_ubyte * _buffer_size), + ('outbuflen', ctypes.c_size_t), + ('char_counter', ctypes.c_ulong), + ('retry_counter', ctypes.c_ulong), + ('counter', ctypes.c_uint), + ('errout', GpsdErrOutT), + ] + + +def new(): + """new() -> new packet-self object""" + return Lexer() + + +def register_report(reporter): + """register_report(callback) + + callback must be a callable object expecting a string as parameter.""" + global _loaded + if callable(reporter): + _loaded.errout.report = REPORTER(reporter) + + +class Lexer(): + """GPS packet lexer object + +Fetch a single packet from file descriptor +""" + pointer = None + + def __init__(self): + global _loaded + _packet.ffi_Lexer_init.restype = ctypes.POINTER(lexer_t) + self.pointer = _packet.ffi_Lexer_init() + _loaded = self.pointer.contents + + def get(self, file_handle): + """Get a packet from a file descriptor.""" + global _loaded + _packet.packet_get.restype = ctypes.c_int + _packet.packet_get.argtypes = [ctypes.c_int, ctypes.POINTER(lexer_t)] + length = _packet.packet_get(file_handle, self.pointer) + _loaded = self.pointer.contents + packet = '' + for octet in range(_loaded.outbuflen): + packet += chr(_loaded.outbuffer[octet]) + return [length, + _loaded.packet_type, + gps.misc.polybytes(packet), + _loaded.char_counter] + + def reset(self): + """Reset the packet self to ground state.""" + _packet.ffi_Lexer_init.restype = None + _packet.ffi_Lexer_init.argtypes = [ctypes.POINTER(lexer_t)] + _packet.ffi_Lexer_init(self.pointer) + +# vim: set expandtab shiftwidth=4 diff --git a/tools/gps/watch_options.py b/tools/gps/watch_options.py new file mode 100644 index 00000000..c6ba885d --- /dev/null +++ b/tools/gps/watch_options.py @@ -0,0 +1,16 @@ +"WATCH options - controls what data is streamed, and how it's converted" +WATCH_ENABLE = 0x000001 # enable streaming +WATCH_DISABLE = 0x000002 # disable watching +WATCH_JSON = 0x000010 # JSON output +WATCH_NMEA = 0x000020 # output in NMEA +WATCH_RARE = 0x000040 # output of packets in hex +WATCH_RAW = 0x000080 # output of raw packets + +WATCH_SCALED = 0x000100 # scale output to floats +WATCH_TIMING = 0x000200 # timing information +WATCH_DEVICE = 0x000800 # watch specific device +WATCH_SPLIT24 = 0x001000 # split AIS Type 24s +WATCH_PPS = 0x002000 # enable PPS JSON + +WATCH_NEWSTYLE = 0x010000 # force JSON streaming +WATCH_OLDSTYLE = 0x020000 # force old-style streaming diff --git a/tools/install.sh b/tools/install.sh new file mode 100755 index 00000000..8c554d05 --- /dev/null +++ b/tools/install.sh @@ -0,0 +1,344 @@ +#!/bin/bash + +### RTKBASE INSTALLATION SCRIPT ### +declare -a detected_gnss + +man_help(){ + echo '################################' + echo 'RTKBASE INSTALLATION HELP' + echo '################################' + echo 'Bash scripts to install a simple gnss base station with a web frontend.' + echo '' + echo '' + echo '' + echo '* Before install, connect your gnss receiver to raspberry pi/orange pi/.... with usb or uart.' + echo '* Running install script with sudo' + echo '' + echo ' sudo ./install.sh' + echo '' + echo 'Options:' + echo ' --all' + echo ' Install all dependencies, Rtklib, last release of Rtkbase, services,' + echo ' crontab jobs, detect your GNSS receiver and configure it.' + echo '' + echo ' --dependencies' + echo ' Install all dependencies like git build-essential python3-pip ...' + echo '' + echo ' --rtklib' + echo ' Clone RTKlib 2.4.3 from github and compile it.' + echo ' https://github.com/tomojitakasu/RTKLIB/tree/rtklib_2.4.3' + echo '' + echo ' --rtkbase-release' + echo ' Get last release of RTKBASE:' + echo ' https://github.com/Stefal/rtkbase/releases' + echo '' + echo ' --rtkbase-repo' + echo ' Clone RTKBASE from github:' + echo ' https://github.com/Stefal/rtkbase/tree/web_gui' + echo '' + echo ' --unit-files' + echo ' Deploy services.' + echo '' + echo ' --gpsd-chrony' + echo ' Install gpsd and chrony to set date and time' + echo ' from the gnss receiver.' + echo '' + echo ' --detect-usb-gnss' + echo ' Detect your GNSS receiver.' + echo '' + echo ' --configure-gnss' + echo ' Configure your GNSS receiver.' + echo '' + echo ' --start-services' + echo ' Start services (rtkbase_web, str2str_tcp, gpsd, chrony)' + exit 0 +} + +install_dependencies() { + echo '################################' + echo 'INSTALLING DEPENDENCIES' + echo '################################' + apt-get update + apt-get install -y git build-essential pps-tools python3-pip python3-dev python3-setuptools python3-wheel libsystemd-dev bc dos2unix socat zip unzip +} + +install_gpsd_chrony() { + echo '################################' + echo 'CONFIGURING FOR USING GPSD + CHRONY' + echo '################################' + apt-get install chrony + #Disabling and masking systemd-timesyncd + systemctl stop systemd-timesyncd + systemctl disable systemd-timesyncd + systemctl mask systemd-timesyncd + #Adding GPS as source for chrony + grep -q 'set larger delay to allow the GPS' /etc/chrony/chrony.conf || echo '# set larger delay to allow the GPS source to overlap with the other sources and avoid the falseticker status +' >> /etc/chrony/chrony.conf + grep -qxF 'refclock SHM 0 refid GPS precision 1e-1 offset 0 delay 0.2' /etc/chrony/chrony.conf || echo 'refclock SHM 0 refid GPS precision 1e-1 offset 0 delay 0.2' >> /etc/chrony/chrony.conf + #Adding PPS as an optionnal source for chrony + grep -q 'refclock PPS /dev/pps0 refid PPS lock GPS' /etc/chrony/chrony.conf || echo '#refclock PPS /dev/pps0 refid PPS lock GPS' >> /etc/chrony/chrony.conf + + #Overriding chrony.service with custom dependency + cp /lib/systemd/system/chrony.service /etc/systemd/system/chrony.service + sed -i s/^After=.*/After=gpsd.service/ /etc/systemd/system/chrony.service + + #If needed, adding backports repository to install a gpsd release that support the F9P + if lsb_release -c | grep -qE 'bionic|buster' + then + if ! apt-cache policy | grep -qE 'buster-backports.* armhf' + then + #Adding buster-backports + echo 'deb http://httpredir.debian.org/debian buster-backports main contrib' > /etc/apt/sources.list.d/backports.list + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 648ACFD622F3D138 + apt-get update + fi + apt-get -t buster-backports install gpsd -y + else + #We hope that the release is more recent than buster and provide gpsd 3.20 or > + apt-get install gpsd -y + fi + #disable hotplug + sed -i 's/^USBAUTO=.*/USBAUTO="false"/' /etc/default/gpsd + #Setting correct input for gpsd + sed -i 's/^DEVICES=.*/DEVICES="tcp:\/\/127.0.0.1:5015"/' /etc/default/gpsd + #Adding example for using pps + sed -i '/^DEVICES=.*/a #DEVICES="tcp:\/\/127.0.0.1:5015 \/dev\/pps0"' /etc/default/gpsd + #gpsd should always run, in read only mode + sed -i 's/^GPSD_OPTIONS=.*/GPSD_OPTIONS="-n -b"/' /etc/default/gpsd + #Overriding gpsd.service with custom dependency + cp /lib/systemd/system/gpsd.service /etc/systemd/system/gpsd.service + sed -i 's/^After=.*/After=str2str_tcp.service/' /etc/systemd/system/gpsd.service + if grep -qxF '^BindsTo=' /etc/systemd/system/gpsd.service + then + #Change the BindsTo value + sed -i 's/^BindsTo=.*/BindsTo=str2str_tcp.service/' /etc/systemd/system/gpsd.service + else + #Add the BindsTo value + sed -i '/^After=.*/i BindsTo=str2str_tcp.service' /etc/systemd/system/gpsd.service + fi + + #Reload systemd services and enable chrony and gpsd + systemctl daemon-reload + systemctl enable gpsd + systemctl enable chrony + #Enable chrony can fail but it works, so let's return 0 to not break the script. + return 0 +} + +install_rtklib() { + echo '################################' + echo 'INSTALLING RTKLIB' + echo '################################' + # str2str already exist? + if [ ! -f /usr/local/bin/str2str ] + then + rm -rf RTKLIB/ + #Get Rtklib 2.4.3 repository + sudo -u $(logname) git clone -b rtklib_2.4.3 --single-branch https://github.com/tomojitakasu/RTKLIB + #Install Rtklib app + #TODO add correct CTARGET in makefile? + make --directory=RTKLIB/app/str2str/gcc + make --directory=RTKLIB/app/str2str/gcc install + make --directory=RTKLIB/app/rtkrcv/gcc + make --directory=RTKLIB/app/rtkrcv/gcc install + make --directory=RTKLIB/app/convbin/gcc + make --directory=RTKLIB/app/convbin/gcc install + #deleting RTKLIB + rm -rf RTKLIB/ + else + echo 'str2str already exist' + fi +} + +rtkbase_repo(){ + #Get rtkbase repository + sudo -u $(logname) git clone -b web_gui --single-branch https://github.com/stefal/rtkbase.git + sudo -u $(logname) touch rtkbase/settings.conf + +} + +rtkbase_release(){ + #Get rtkbase latest release + sudo -u $(logname) wget https://github.com/stefal/rtkbase/releases/latest/download/rtkbase.tar.gz -O rtkbase.tar.gz + sudo -u $(logname) tar -xvf rtkbase.tar.gz + sudo -u $(logname) touch rtkbase/settings.conf + +} + +install_rtkbase_from_repo() { + echo '################################' + echo 'INSTALLING RTKBASE FROM REPO' + echo '################################' + if [ -d rtkbase ] + then + if [ -d rtkbase/.git ] + then + echo "RtkBase repo: YES, git pull" + git -C rtkbase pull + else + echo "RtkBase repo: NO, rm release & git clone rtkbase" + rm -r rtkbase + rtkbase_repo + fi + else + echo "RtkBase repo: NO, git clone rtkbase" + rtkbase_repo + fi +} + +install_rtkbase_from_release() { + echo '################################' + echo 'INSTALLING RTKBASE FROM RELEASE' + echo '################################' + if [ -d rtkbase ] + then + if [ -d rtkbase/.git ] + then + echo "RtkBase release: NO, rm repo & download last release" + rm -r rtkbase + rtkbase_release + else + echo "RtkBase release: YES, rm & deploy last release" + rtkbase_release + fi + else + echo "RtkBase release: NO, download & deploy last release" + rtkbase_release + fi +} + +rtkbase_requirements(){ + echo '################################' + echo 'INSTALLING RTKBASE REQUIREMENTS' + echo '################################' + #as we need to run the web server as root, we need to install the requirements with + #the same user + python3 -m pip install --upgrade pip setuptools wheel --extra-index-url https://www.piwheels.org/simple + python3 -m pip install -r rtkbase/web_app/requirements.txt --extra-index-url https://www.piwheels.org/simple + # We were waiting for the next pystemd official release. + # install pystemd dev wheel for arm platform + python3 -m pip install rtkbase/tools/pystemd-0.8.1590398158-cp37-cp37m-linux_armv7l.whl + #when we will be able to launch the web server without root, we will use + #sudo -u $(logname) python3 -m pip install -r requirements.txt --user. +} + +install_unit_files() { + echo '################################' + echo 'ADDING UNIT FILES' + echo '################################' + if [ -d rtkbase ] + then + #Install unit files + rtkbase/copy_unit.sh + systemctl enable rtkbase_web.service + systemctl enable rtkbase_archive.timer + systemctl daemon-reload + else + echo 'RtkBase not installed, use option --rtkbase-release' + fi +} + +detect_usb_gnss() { + echo '################################' + echo 'GNSS RECEIVER DETECTION' + echo '################################' + #This function put the (USB) detected gnss receiver informations in detected_gnss + #If there are several receiver, only the last one will be present in the variable + for sysdevpath in $(find /sys/bus/usb/devices/usb*/ -name dev); do + syspath="${sysdevpath%/dev}" + devname="$(udevadm info -q name -p $syspath)" + if [[ "$devname" == "bus/"* ]]; then continue; fi + eval "$(udevadm info -q property --export -p $syspath)" + if [[ -z "$ID_SERIAL" ]]; then continue; fi + if [[ "$ID_SERIAL" =~ (u-blox|skytraq) ]] + then + detected_gnss[0]=$devname + detected_gnss[1]=$ID_SERIAL + echo '/dev/'${detected_gnss[0]} ' - ' ${detected_gnss[1]} + fi + done +} + +configure_gnss(){ + echo '################################' + echo 'CONFIGURE GNSS RECEIVER' + echo '################################' + if [ -d rtkbase ] + then + if [[ ${#detected_gnss[*]} -eq 2 ]] + then + echo 'GNSS RECEIVER DETECTED: /dev/'${detected_gnss[0]} ' - ' ${detected_gnss[1]} + if [[ ${detected_gnss[1]} =~ 'u-blox' ]] + then + gnss_format='ubx' + fi + if [[ -f "rtkbase/settings.conf" ]] && grep -E "^com_port=.*" rtkbase/settings.conf #check if settings.conf exists + then + #change the com port value inside settings.conf + sudo -u $(logname) sed -i s/^com_port=.*/com_port=\'${detected_gnss[0]}\'/ rtkbase/settings.conf + else + #create settings.conf with the com_port setting and the settings needed to start str2str_tcp + #as it could start before the web server merge settings.conf.default and settings.conf + sudo -u $(logname) printf "[main]\ncom_port='"${detected_gnss[0]}"'\ncom_port_settings='115200:8:n:1'\nreceiver_format='"${gnss_format}"'\ntcp_port='5015'\n" > rtkbase/settings.conf + fi + fi + #if the receiver is a U-Blox, launch the set_zed-f9p.sh. This script will reset the F9P and configure it with the corrects settings for rtkbase + if [[ ${detected_gnss[1]} =~ 'u-blox' ]] + then + rtkbase/tools/set_zed-f9p.sh /dev/${detected_gnss[0]} 115200 rtkbase/receiver_cfg/U-Blox_ZED-F9P_rtkbase.txt + fi + else + echo 'RtkBase not installed, use option --rtkbase-release' + fi +} + +start_services() { + echo '################################' + echo 'STARTING SERVICES' + echo '################################' + systemctl daemon-reload + systemctl start rtkbase_web.service + systemctl start str2str_tcp.service + systemctl restart gpsd.service + systemctl restart chrony.service + systemctl start rtkbase_archive.timer + echo '################################' + echo 'END OF INSTALLATION' + echo 'You can open your browser to http://'$(hostname -I) + echo '################################' +} +main() { + #display parameters + echo 'Installation options: ' $@ + array=($@) + # if no parameters display help + if [ -z "$array" ] ; then man_help ;fi + # run intall options + for i in "${array[@]}" + do + if [ "$1" == "--help" ] ; then man_help ;fi + if [ "$i" == "--dependencies" ] ; then install_dependencies ;fi + if [ "$i" == "--rtklib" ] ; then install_rtklib ;fi + if [ "$i" == "--rtkbase-release" ]; then install_rtkbase_from_release && \ + rtkbase_requirements ;fi + if [ "$i" == "--rtkbase-repo" ] ; then install_rtkbase_from_repo && \ + rtkbase_requirements ;fi + if [ "$i" == "--unit-files" ] ; then install_unit_files ;fi + if [ "$i" == "--gpsd-chrony" ] ; then install_gpsd_chrony ;fi + if [ "$i" == "--detect-usb-gnss" ]; then detect_usb_gnss ;fi + if [ "$i" == "--configure-gnss" ] ; then configure_gnss ;fi + if [ "$i" == "--start-services" ] ; then start_services ;fi + if [ "$i" == "--all" ] ; then install_dependencies && \ + install_rtklib && \ + install_rtkbase_from_release && \ + rtkbase_requirements && \ + install_unit_files && \ + install_gpsd_chrony && \ + detect_usb_gnss && \ + configure_gnss && \ + start_services ;fi + done +} + +main $@ +exit 0 diff --git a/tools/pystemd-0.8.1590398158-cp37-cp37m-linux_armv7l.whl b/tools/pystemd-0.8.1590398158-cp37-cp37m-linux_armv7l.whl new file mode 100644 index 00000000..6d57ac08 Binary files /dev/null and b/tools/pystemd-0.8.1590398158-cp37-cp37m-linux_armv7l.whl differ diff --git a/tools/set_zed-f9p.sh b/tools/set_zed-f9p.sh new file mode 100755 index 00000000..0940a4ca --- /dev/null +++ b/tools/set_zed-f9p.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +### Script to configure a U-Blox Zed-F9P ### + +BASEDIR=$(dirname "$0") + +export GPS=$1 +export DEVICE_SPEED=$2 +export CONFIG=$3 + +set_F9P() { + if [[ $(python3 ${BASEDIR}/ubxtool -p MON-VER) =~ 'ZED-F9P' ]] + then + echo 'U-Blox ZED-F9P detected' + echo 'Resetting ZED-F9P to default settings' + python3 ${BASEDIR}/ubxtool -p RESET + sleep 5 + #Now the default speed is 38400. Change it to 115200 + #It is unuseful for a Usb connexion but needed with a UART. + echo 'Set UART Baudrate....' + python3 ${BASEDIR}/ubxtool -s 38400 -z CFG-UART1-BAUDRATE,115200 + + echo 'Sending settings....' + while read setting; do + python3 ${BASEDIR}/ubxtool -s 115200 -z $setting + done <${CONFIG} + echo 'Done' + fi +} + +#echo $GPS +#echo $DEVICE_SPEED +#echo $CONFIG + +if [[ -c ${GPS} ]] && [[ ${DEVICE_SPEED} -gt 0 ]] && [[ -f $CONFIG && -s $CONFIG && $CONFIG == *.txt ]] +then + #Overwrite UBXOPTS with the settings from the command line + export UBXOPTS="-f ${GPS} -s ${DEVICE_SPEED} -v 0" + set_F9P +else + echo "usage: set_zed-f9p.sh device baudrate config_file.txt" + echo "example: set_zed-f9p.sh /dev/ttyACM0 115200 config_file.txt" + exit 1 +fi +exit 0 \ No newline at end of file diff --git a/ubxconfig.sh b/tools/ubxconfig.sh similarity index 100% rename from ubxconfig.sh rename to tools/ubxconfig.sh diff --git a/tools/ubxtool b/tools/ubxtool new file mode 100755 index 00000000..7bc36958 --- /dev/null +++ b/tools/ubxtool @@ -0,0 +1,7870 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 +# This code is generated by scons. Do not hand-hack it! +''' +ubxtool -- u-blox configurator and packet decoder + +usage: ubxtool [OPTIONS] [server[:port[:device]]] +''' + +# This file is Copyright 2018 by the GPSD project +# SPDX-License-Identifier: BSD-2-clause +# +# This code runs compatibly under Python 2 and 3.x for x >= 2. +# Preserve this property! +# +# ENVIRONMENT: +# Options in the UBXOPTS environment variable will be parsed before +# the CLI options. A handy place to put your '-f /dev/ttyXX -s SPEED' +# +# To see what constellations are enabled: +# ubxtool -p CFG-GNSS -f /dev/ttyXX +# +# To disable GLONASS and enable GALILEO: +# ubxtool -d GLONASS -f /dev/ttyXX +# ubxtool -e GALILEO -f /dev/ttyXX +# +# To read GPS messages a log file: +# ubxtool -v 2 -f test/daemon/ublox-neo-m8n.log +# +# References: +# [1] IS-GPS-200K + +from __future__ import absolute_import, print_function, division + +import binascii # for binascii.hexlify() +from functools import reduce # pylint: disable=redefined-builtin +import getopt # for getopt.getopt(), to parse CLI options +import operator # for or_ +import os # for os.environ +import re # for regular expressions +import socket # for socket.error +import stat # for stat.S_ISBLK() +import string # for string.printable +import struct # for pack() +import sys +import time + +PROG_NAME = 'ubxtool' + +try: + import serial +except ImportError: + serial = None # Defer complaining until we know we need it. + +try: + import gps +except ImportError: + # PEP8 says local imports last + sys.stderr.write("%s: failed to import gps, check PYTHONPATH\n" % + PROG_NAME) + sys.exit(2) + +gps_version = '3.20.1~dev' +if gps.__version__ != gps_version: + sys.stderr.write("%s: ERROR: need gps module version %s, got %s\n" % + (PROG_NAME, gps_version, gps.__version__)) + sys.exit(1) + +# Some old versions of Python fail to accept a bytearray as an input to +# struct.unpack_from, though it can be worked around by wrapping it with +# buffer(). Since the fix is only needed in rare cases, this monkey-patches +# struct.unpack_from() when needed, and otherwise changes nothing. If +# struct.unpack() were used, it would need similar treatment, as would +# methods from struct.Struct if that were used. +try: + struct.unpack_from('B', bytearray(1)) +except TypeError: + unpack_from_orig = struct.unpack_from + + def unpack_from_fixed(fmt, buf, offset=0): + return unpack_from_orig(fmt, buffer(buf), offset=offset) + + struct.unpack_from = unpack_from_fixed + +VERB_QUIET = 0 # quiet +VERB_NONE = 1 # just output requested data and some info +VERB_DECODE = 2 # decode all messages +VERB_INFO = 3 # more info +VERB_RAW = 4 # raw info +VERB_PROG = 5 # program trace + +# dictionary to hold all user options +opts = { + # command to send to GPS, -c + 'command': None, + # default -x items, up to 64 per call + 'del_item': [], + # command for -d disable + 'disable': None, + # command for -e enable + 'enable': None, + # help requested + 'help': None, + # default input -f file + 'input_file_name': None, + # default -g items, up to 64 per call + 'get_item': [], + # default forced wait? -W + 'input_forced_wait': False, + # default port speed -s + 'input_speed': 9600, + # default input wait time -w in seconds + 'input_wait': 2.0, + # optional mode to -p P + 'mode': None, + # the name of an OAF file, extension .jpo + 'oaf_name': None, + # optional argument to -d/-e + 'parm1': None, + # poll command -p + 'poll': None, + # port for port-related commands + 'port': None, + # protocol version for sent commands + # u-blox 5, firmware 4 to 6 is protver 10 to 12 + # u-blox 6, firmware 6 to 7 is protver 12 to 13 + # u-blox 6, firmware 1 is protver 14 + # u-blox 7, firmware 1 is protver 14 + # u-blox 8, is protver 15 to 23 + # u-blox 9, firmware 1 is protver 27 + # u-blox F9T, firmware 2 is protver 29 + # u-blox F9N, firmware 4 is protver 32 + 'protver': 10.0, + # raw log file name + 'raw_file': None, + # open port read only -r + 'read_only': False, + # default -z item + 'set_item': [], + # speed to set GPS -S + 'set_speed': None, + # target gpsd (server:port:device) to connect to + 'target': {"server": None, "port": gps.GPSD_PORT, "device": None}, + # verbosity level, -v + 'verbosity': VERB_NONE, + # contents of environment variable UBXOPTS + 'progopts': '', +} + + +# I'd like to use pypy module bitstring or bitarray, but +# people complain when non stock python modules are used here. +def unpack_s11(word, pos): + """Grab a signed 11 bits from offset pos of word""" + + ubytes = bytearray(2) + ubytes[0] = (word >> pos) & 0xff + ubytes[1] = (word >> (pos + 8)) & 0x07 + if 0x04 & ubytes[1]: + # extend the sign + ubytes[1] |= 0xf8 + u = struct.unpack_from('> 22) & 0xff + newword <<= 3 + newword |= (word >> 8) & 0x07 + return unpack_s11(newword, 0) + + +def unpack_s14(word, pos): + """Grab a signed 14 bits from offset pos of word""" + + ubytes = bytearray(2) + ubytes[0] = (word >> pos) & 0xff + ubytes[1] = (word >> (pos + 8)) & 0x3f + if 0x20 & ubytes[1]: + # extend the sign + ubytes[1] |= 0xc0 + u = struct.unpack_from('> pos) & 0xff + ubytes[1] = (word >> (pos + 8)) & 0xff + u = struct.unpack_from('> pos) & 0xff + ubytes[1] = (word >> (pos + 8)) & 0xff + u = struct.unpack_from('> pos) & 0xff + ubytes[1] = (word >> (pos + 8)) & 0xff + ubytes[2] = (word >> (pos + 16)) & 0x3f + ubytes[3] = 0 + if 0x20 & ubytes[2]: + # extend the sign + ubytes[2] |= 0xc0 + ubytes[3] = 0xff + + u = struct.unpack_from('> pos) & 0xff + ubytes[1] = (word >> (pos + 8)) & 0xff + ubytes[2] = (word >> (pos + 16)) & 0xff + ubytes[3] = 0 + if 0x80 & ubytes[2]: + # extend the sign + ubytes[3] = 0xff + + u = struct.unpack_from('> pos) & 0xff + ubytes[1] = (word >> (pos + 8)) & 0xff + ubytes[2] = (word >> (pos + 16)) & 0xff + ubytes[3] = 0 + + u = struct.unpack_from('> 6) & 0xff + ubytes[1] = (word >> 14) & 0xff + ubytes[2] = (word >> 22) & 0xff + ubytes[3] = (word1 >> 6) & 0xff + + u = struct.unpack_from('> 6) & 0xff + ubytes[1] = (word >> 14) & 0xff + ubytes[2] = (word >> 22) & 0xff + ubytes[3] = (word1 >> 6) & 0xff + + u = struct.unpack_from('> pos) & 0xff + u = struct.unpack_from('> pos) & 0xff + u = struct.unpack_from('> 28) & 0x07 + cfg_type = key_map[key_size] + + return cfg_type + + def cfg_by_key(self, key): + """Find a config item by key""" + + for item in self.cfgs: + if item[1] == key: + return item + + # not found, build a fake item, guess on decode + name = "CFG-%u-%u" % ((key >> 16) & 0xff, key & 0xff) + kmap = {0: "Z0", + 1: "L", + 2: "U1", + 3: "U2", + 4: "U4", + 5: "U8", + 6: "Z6", + 7: "Z7", + } + size = (key >> 28) & 0x07 + item = (name, key, kmap[size], 1, "Unk", "Unknown") + + return item + + def cfg_by_name(self, name): + """Find a config item by name""" + + for item in self.cfgs: + if item[0] == name: + return item + + return None + + id_map = { + 0: {"name": "GPS", + "sig": {0: "L1C/A", 3: "L2 CL", 4: "L2 CM"}}, + 1: {"name": "SBAS", + "sig": {0: "L1C/A", 3: "L2 CL", 4: "L2 CM"}}, + 2: {"name": "Galileo", + "sig": {0: "E1C", 1: "E1 B", 5: "E5 bl", 6: "E5 bQ"}}, + 3: {"name": "BeiDou", + "sig": {0: "B1I D1", 1: "B1I D2", 2: "B2I D1", 3: "B2I D2"}}, + 4: {"name": "IMES", + "sig": {0: "L1C/A", 3: "L2 CL", 4: "L2 CM"}}, + 5: {"name": "QZSS", + "sig": {0: "L1C/A", 4: "L2 CM", 5: "L2 CL"}}, + 6: {"name": "GLONASS", + "sig": {0: "L1 OF", 2: "L2 OF"}}, + } + + def gnss_s(self, gnssId, svId, sigId): + """Verbose decode of gnssId, svId and sigId""" + + s = '' + + if gnssId in self.id_map: + if "name" not in self.id_map[gnssId]: + s = "%d:%d:%d" % (gnssId, svId, sigId) + elif sigId not in self.id_map[gnssId]["sig"]: + s = ("%s:%d:%d" % + (self.id_map[gnssId]["name"], svId, sigId)) + else: + s = ("%s:%d:%s" % + (self.id_map[gnssId]["name"], svId, + self.id_map[gnssId]["sig"][sigId])) + else: + s = "%d:%d:%d" % (gnssId, svId, sigId) + + return s + + def ack_ack(self, buf): + """UBX-ACK-ACK decode""" + + # NOTE: Not all messages to u-blox GPS are ACKed... + + u = struct.unpack_from(' m_len: + return " Bad Length %s" % m_len + + u = struct.unpack_from(' m_len: + return " Bad Length %s" % m_len + + u = struct.unpack_from(' m_len: + return " Bad Length %s" % m_len + + u = struct.unpack_from('> 5) & 0x1f, (u[1] >> 10) & 0x1f, + u[1] >> 15)) + if VERB_DECODE <= opts['verbosity']: + s += ('\n flags (%s)' % flag_s(u[0], self.cfg_ant_pins)) + + return s + + cfg_batch_flags = { + 1: 'enable', + 4: 'extraPvt', + 8: 'extraOdo', + 0x20: 'pioEnable', + 0x40: 'pioActiveLow', + } + + def cfg_batch(self, buf): + """UBX-CFG-BATCH decode""" + + u = struct.unpack_from(' m_len: + s = " Bad Length %d" % m_len + + elif 44 == m_len: + # set user defined datum + u = struct.unpack_from(' m_len: + s = " Bad Length %d" % m_len + + elif 52 <= m_len: + # get user defined datum + u = struct.unpack_from(' sat: + s += (" %s %s " % + (index_s(sat, self.gnss_id), + flag_s(u[4], self.cfg_gnss_sig[sat]))) + else: + s += "Unk " + + if u[4] & 0x01: + s += 'enabled' + + return s + + def cfg_hnr(self, buf): + """UBX-CFG-HNR decode, High Navigation Rate Settings""" + + u = struct.unpack_from('> 4) & 0x1f, (u[0] >> 9) & 0x1fffff, + flag_s(buf[1], self.cfg_itfm_config2), + u[1] & 0x0fff, + index_s((u[1] >> 12) & 3, self.cfg_itfm_config2))) + return s + + cfg_logfilter_flags = { + 1: "recordEnabled", + 2: "psmOncePerWakupEnabled", + 4: "applyAllFilterSettings", + } + + def cfg_logfilter(self, buf): + """UBX-CFG-LOGFILTER decode, Data Logger Configuration""" + + # u-blox 7+, protVer 14+ + u = struct.unpack_from('> 4, self.utc_std), + flag_s(u[0] >> 4, self.cfg_nav5_mask))) + return s + + cfg_navx5_mask1 = { + 4: "minMax", + 8: "minCno", + 0x40: "initial3dfix", + 0x200: "wknRoll", + 0x400: "ackAid", + 0x2000: "ppp", + 0x4000: "aop", + } + + cfg_navx5_mask2 = { + 0x40: "adr", + 0x80: "sigAttenComp", + } + + cfg_navx5_aop = { + 1: "useAOP", + } + + def cfg_navx5(self, buf): + """UBX-CFG-NAVX5 decode, Navigation Engine Expert Settings""" + + # deprecated protver 23+ + # length == 20 case seems broken? + m_len = len(buf) + + u = struct.unpack_from('> 4) & 1, + index_s((u[4] >> 8) & 3, self.cfg_pm_limitPeakCurr))) + return s + + cfg_pm2_mode = { + 0: "ON/OFF operation (PSMOO)", + 1: "Cyclic tracking operation (PSMCT)", + 2: "reserved", + 3: "reserves3", + } + + cfg_pm2_optTarget = { + 0: "performance", + 1: "power save", + } + + def cfg_pm2(self, buf): + """UBX-CFG-PM2 decode, Extended Power Mode Configuration""" + + # three versions, two lengths + # "version" 1 is 44 bytes + # "version" 2 is 48 bytes,protver <= 22 + # "version" 2 is 48 bytes,protver >= 23 + + m_len = len(buf) + + # 48 bytes protver 18+ + + u = struct.unpack_from('= m_len: + u1 = struct.unpack_from('> 4) & 1, + index_s((u[4] >> 8) & 3, self.cfg_pm_limitPeakCurr), + index_s((u[4] >> 1) & 3, self.cfg_pm2_optTarget, + nf="reserved"), + index_s((u[4] >> 17) & 3, self.cfg_pm2_mode))) + + return s + + cfg_pms_values = {0: "Full power", + 1: "Balanced", + 2: "Interval", + 3: "Aggresive with 1Hz", + 4: "Aggresive with 2Hz", + 5: "Aggresive with 4Hz", + 0xff: "Invalid" + } + + def cfg_pms(self, buf): + """UBX-CFG-PMS decode, Power Mode Setup""" + + u = struct.unpack_from(' m_len: + return " Bad Length %s" % m_len + + u = struct.unpack_from('> 1 & 0x7F)) + + s.append(' inProtoMask (%s)\n' + ' outProtoMask (%s)' % + (flag_s(u[5], self.cfg_prt_proto), + flag_s(u[6], self.cfg_prt_proto))) + + if portid in set([1, 2, 4, 0]): + s.append(' flags (%s)' % flag_s(u[7], self.cfg_prt_flags)) + + return '\n'.join(s) + + cfg_pwr_state = { + 0x52554E20: "GNSS running", + 0x53544F50: "GNSS stopped", + 0x42434B50: "Software Backup", + } + + def cfg_pwr(self, buf): + """UBX-CFG-PWR decode, Put receiver in a defined power state""" + + u = struct.unpack_from(' m_len: + return " Bad Length %s" % m_len + + u = struct.unpack_from('> 7) & 0x0f, self.cfg_tp5_grid), + (u[10] >> 11) & 0x03)) + + return s + + cfg_usb_flags = {1: "reEnum "} + + cfg_usb_powerMode = {0: "self-powered", + 2: "bus-powered", + } + + def cfg_usb(self, buf): + """UBX-CFG-USB decode, USB Configuration""" + + u = struct.unpack_from(' opts['protver']: + opts['protver'] = 27 + + # sort of duplicated in cfg_valset() + i += 4 + while 4 < m_len: + u = struct.unpack_from(' m_len: + s += "\nWARNING: not enough bytes!" + break; + + frmat = cfg_type[1] + flavor = cfg_type[2] + v = struct.unpack_from(frmat, buf, i) + s += ('\n item %s/%#x val %s' % (item[0], u[0], v[0])) + m_len -= size + i += size + + if 0 < m_len: + s += "\nWARNING: %d extra bytes!" % m_len + + return s + + def cfg_valset(self, buf): + """"UBX-CFG-VALSET decode, Set configuration items""" + m_len = len(buf) + + # this is a poll option, so does not set min protver + + u = struct.unpack_from(' opts['protver']: + opts['protver'] = 27 + + u = struct.unpack_from(' m_len: + return " Bad Length %s" % m_len + + u = struct.unpack_from('> 2) & 0x03, self.jammingState))) + return s + + mon_hw2_cfgSource = { + 102: "flash image", + 111: "OTP", + 112: "config pins", + 114: "ROM", + } + + def mon_hw2(self, buf): + """UBX-MON-HW2 decode, Extended Hardware Status""" + + u = struct.unpack_from(' opts['protver']: + opts['protver'] = 27 + + u = struct.unpack_from('> 1) & 7, self.mon_hw3_bank), + index_s((u[1] >> 4) & 1, self.mon_hw3_dir), + index_s((u[1] >> 5) & 1, self.mon_hw3_value), + flag_s(u[1] & 0xffc0, self.mon_hw3_mask))) + + return s + + def mon_io(self, buf): + """UBX-MON-IO decode, I/O Subsystem Status""" + m_len = len(buf) + + s = '' + for i in range(0, int(m_len / 20)): + if 0 < i: + s += "\n" + + u = struct.unpack_from(' opts['protver']: + opts['protver'] = 27 + + u = struct.unpack_from('> 4) & 1)) + + return s + + def nav_dop(self, buf): + """UBX-NAV-DOP decode, Dilution of Precision""" + + u = struct.unpack_from(' 30 days", + 31: "Unknown", + } + + nav_orb_ephUsability = { + 0: "Unusable", + 30: "> 450 mins", + 31: "Unknown", + } + + nav_orb_ephSource = { + 0: "not available", + 1: "GNSS transmission", + 2: "external aiding", + } + + nav_orb_type = { + 0: "not available", + 1: "Assist now offline data", + 2: "Assist now autonomous data", + } + + def nav_orb(self, buf): + """UBX-NAV-ORB decode, GNSS Orbit Database Info""" + + u = struct.unpack_from('> 2) & 3, self.visibility), + s1, + index_s(u[3] >> 5, self.nav_orb_ephSource, nf="other"), + s2, + index_s(u[4] >> 5, self.nav_orb_ephSource, nf="other"), + s3, + index_s(u[5] >> 5, self.nav_orb_type, nf="other"))) + + return s + + def nav_posecef(self, buf): + """UBX-NAV-POSECEF decode, Position Solution in ECEF""" + + # protVer 4+ + u = struct.unpack_from('> 2) & 0x0f, self.nav_pvt_psm), + index_s((u[11] >> 6) & 0x03, self.carrSoln))) + return s + + nav_relposned_flags = { + 1: "gnssFixOK", + 2: "diffSoln", + 4: "relPosValid", + 0x20: "isMoving", # protVer 20.3+ + 0x40: "refPosMiss", # protVer 20.3+ + 0x80: "refObsMiss", # protVer 20.3+ + 0x100: "relPosHeadingValid", # protVer 27.11+ + 0x200: "relPosNormalized", # protVer 27.11+ + } + + def nav_relposned(self, buf): + """UBX-NAV-RELPOSNED decode +Relative Positioning Information in NED frame. +protVer 20+ is 40 bytes +protVer 27.11+ is 64 bytes, and things reordered, so not upward compatible +High Precision GNSS products only.""" + + m_len = len(buf) + + # common part + u = struct.unpack_from('> 3) & 0x03, self.carrSoln))) + return s + + def nav_resetodo(self, buf): + """UBX-NAV-RESETODO decode, reset odometer""" + + m_len = len(buf) + if 0 == m_len: + s = " reset request" + else: + s = " unexpected data" + return s + + qualityInd = { + 0: "None", + 1: "Searching", + 2: "Acquired", + 3: "Detected", + 4: "Code and time locked", + 5: "Code, carrier and time locked", + 6: "Code, carrier and time locked", + 7: "Code, carrier and time locked", + } + + health = { + 0: "Unknown", + 1: "Healthy", + 2: "Unhealthy", + } + + visibility = { + 1: "below horizon", + 2: "above horizon", + 3: "above elevation mask", + } + + nav_sat_orbit = { + 0: "None", + 1: "Ephemeris", + 2: "Almanac", + 3: "AssistNow Offline", + 4: "AssistNow Autonomous", + 5: "Other", + 6: "Other", + 7: "Other", + } + + nav_sat_flags = { + 8: "svUsed", + 0x40: "diffCorr", + 0x80: "smoothed", + 0x800: "ephAvail", + 0x1000: "almAvail", + 0x2000: "anoAvail", + 0x4000: "aopAvail", + 0x10000: "sbasCorrUsed", + 0x20000: "rtcmCorrUsed", + 0x40000: "slasCorrUsed", + 0x100000: "prCorrUsed", + 0x200000: "crCorrUsed", + 0x400000: "doCorrUsed", + } + + def nav_sat(self, buf): + """UBX-NAV-SAT decode""" + + u = struct.unpack_from('> 4) & 3, self.health), + index_s((u[6] >> 8) & 7, self.nav_sat_orbit))) + + return s + + nav_sbas_mode = { + 0: "Disabled", + 1: "Enabled Integrity", + 2: "Enabled Testmode", + } + + # sometimes U1 or I1, 255 or -1 == Unknown + nav_sbas_sys = { + 0: "WAAS", + 1: "EGNOS", + 2: "MSAS", + 3: "GAGAN", + 4: "SDCM", # per ICAO Annex 10, v1, Table B-27 + 16: "GPS", + } + + nav_sbas_service = { + 1: "Ranging", + 2: "Corrections", + 4: "Integrity", + 8: "Testmode", + } + + def nav_sbas(self, buf): + """UBX-NAV-SBAS decode, SBAS Status Data""" + + # present in protver 10+ (Antaris4 to ZOE-M8B + # undocumented, but present in protver 27+ + # undocumented, but present in protver 32, NEO-M9N + + u = struct.unpack_from('> 4, self.utc_std))) + return s + + def nav_velecef(self, buf): + """UBX-NAV-VELECEF decode""" + + # protVer 4+ + u = struct.unpack_from('> 24 + if 0x8b == preamble: + # CNAV + msgid = (words[0] >> 12) & 0x3f + s += ("\n CNAV: preamble %#x PRN %u msgid %d (%s)\n" % + (preamble, (words[0] >> 18) & 0x3f, + msgid, index_s(msgid, self.cnav_msgids))) + + else: + # LNAV-L + preamble = words[0] >> 22 + subframe = (words[1] >> 8) & 0x07 + s += ("\n LNAV-L: preamble %#x TLM %#x ISF %u" % + (preamble, (words[0] >> 8) & 0xffff, + 1 if (words[0] & 0x40) else 0)) + + s += ("\n TOW %u AF %u ASF %u Subframe %u" % + (unpack_u8(words[1], 13) * 6, + 1 if (words[0] & 0x1000) else 0, + 1 if (words[0] & 0x800) else 0, + subframe)) + + if 1 == subframe: + # not well validated decode, possibly wrong... + # [1] Figure 20-1 Sheet 1, Table 20-I + # WN = GPS week number + # TGD = Group Delay Differential + # tOC = Time of Clock + # af0 = SV Clock Bias Correction Coefficient + # af1 = SV Clock Drift Correction Coefficient + # af2 = Drift Rate Correction Coefficient + ura = (words[2] >> 14) & 0x0f + c_on_l2 = (words[2] >> 18) & 0x03 + iodc = ((((words[2] >> 6) & 0x03) << 8) | + (words[7] >> 24) & 0xff) + s += ("\n WN %u Codes on L2 %u (%s) URA %u (%s) " + "SVH %#04x IODC %u" % + (words[2] >> 20, + c_on_l2, index_s(c_on_l2, self.codes_on_l2), + ura, index_s(ura, self.ura_meters), + (words[2] >> 8) & 0x3f, iodc)) + # tOC = Clock Data Reference Time of Week + s += ("\n L2 P DF %u TGD %e tOC %u\n" + " af2 %e af1 %e af0 %e" % + ((words[2] >> 29) & 0x03, + unpack_s8(words[6], 6) * (2 ** -31), + unpack_u16(words[7], 6) * 16, + unpack_s8(words[8], 22) * (2 ** -55), + unpack_s16(words[8], 6) * (2 ** -43), + unpack_s22(words[9], 8) * (2 ** -31))) + + elif 2 == subframe: + # not well validated decode, possibly wrong... + # [1] Figure 20-1 Sheet 1, Tables 20-II and 20-III + # IODE = Issue of Data (Ephemeris) + # Crs = Amplitude of the Sine Harmonic Correction + # Term to the Orbit Radius + # Deltan = Mean Motion Difference From Computed Value + # M0 = Mean Anomaly at Reference Time + # Cuc = Amplitude of the Cosine Harmonic Correction + # Term to the Argument of Latitude + # e = Eccentricity + # Cus = Amplitude of the Sine Harmonic Correction Term + # to the Argument of Latitude + # sqrtA = Square Root of the Semi-Major Axis + # tOE = Reference Time Ephemeris + s += ("\n IODE %u Crs %e Deltan %e M0 %e" + "\n Cuc %e e %e Cus %e sqrtA %f tOE %u" % + (unpack_u8(words[2], 22), + unpack_s16(words[2], 6) * (2 ** -5), + unpack_s16(words[3], 14) * (2 ** -43), + # M0 + unpack_s32s(words[4], words[3]) * (2 ** -31), + unpack_s16(words[5], 14) * (2 ** -29), + unpack_u32s(words[6], words[5]) * (2 ** -33), + unpack_s16(words[7], 14) * (2 ** -29), + unpack_u32s(words[8], words[7]) * (2 ** -19), + unpack_u16(words[9], 14) * 16)) + + elif 3 == subframe: + # not well validated decode, possibly wrong... + # [1] Figure 20-1 Sheet 3, Table 20-II, Table 20-III + # Cic = Amplitude of the Cosine Harmonic Correction + # Term to the Angle of Inclination + # Omega0 = Longitude of Ascending Node of Orbit + # Plane at Weekly Epoch + # Cis = Amplitude of the Sine Harmonic Correction + # Term to the Orbit Radius + # i0 = Inclination Angle at Reference Time + # Crc = Amplitude of the Cosine Harmonic Correction + # Term to the Orbit Radius + # omega = Argument of Perigee + # Omegadot = Rate of Right Ascension + # IODE = Issue of Data (Ephemeris) + # IODT = Rate of Inclination Angle + s += ("\n Cic %e Omega0 %e Cis %e i0 %e" + "\n Crc %e omega %e Omegadot %e" + "\n IDOE %u IDOT %e" % + (unpack_s16(words[2], 14) * (2 ** -29), + unpack_s32s(words[3], words[2]) * (2 ** -31), + unpack_s16(words[4], 14) * (2 ** -29), + unpack_s32s(words[5], words[4]) * (2 ** -31), + # Crc + unpack_s16(words[6], 14) * (2 ** -5), + unpack_s32s(words[7], words[6]) * (2 ** -31), + # Omegadot + unpack_s24(words[8], 6) * (2 ** -43), + unpack_u8(words[9], 22), + unpack_s14(words[9], 8) * (2 ** -43))) + + elif 4 == subframe: + # all data in subframe 4 is "reserved", + # except for pages 13, 18, 15 + # as of 2018, dataid is always 1. + svid = (words[2] >> 22) & 0x3f + if 0 < svid: + page = index_s(svid, self.sbfr4_svid_page) + else: + # page of zero means the svId that sent it. + page = "%d/Self" % svId + + s += ("\n dataid %u svid %u (page %s)\n" % + (words[2] >> 28, svid, page)) + + if 2 <= page <= 10: + s += self.almanac(words) + elif 13 == page: + s += " NWCT" + elif 17 == page: + s += (" Special messages: " + + chr((words[2] >> 14) & 0xff) + + chr((words[2] >> 6) & 0xff) + + chr((words[3] >> 22) & 0xff) + + chr((words[3] >> 14) & 0xff) + + chr((words[3] >> 6) & 0xff) + + chr((words[4] >> 22) & 0xff) + + chr((words[4] >> 14) & 0xff) + + chr((words[4] >> 6) & 0xff) + + chr((words[5] >> 22) & 0xff) + + chr((words[5] >> 14) & 0xff) + + chr((words[5] >> 6) & 0xff) + + chr((words[6] >> 22) & 0xff) + + chr((words[6] >> 14) & 0xff) + + chr((words[6] >> 6) & 0xff) + + chr((words[7] >> 22) & 0xff) + + chr((words[7] >> 14) & 0xff) + + chr((words[7] >> 6) & 0xff) + + chr((words[8] >> 22) & 0xff) + + chr((words[8] >> 14) & 0xff) + + chr((words[8] >> 6) & 0xff) + + chr((words[9] >> 22) & 0xff) + + chr((words[9] >> 14) & 0xff)) + + elif 18 == page: + s += " Ionospheric and UTC data" + elif 25 == page: + s += " A/S flags" + else: + s += " Reserved" + + elif 5 == subframe: + svid = (words[2] >> 22) & 0x3f + page = index_s(svid, self.sbfr5_svid_page) + + s += ("\n dataid %u svid %u (page %s)\n" % + (words[2] >> 28, svid, page)) + + if 1 <= page <= 24: + s += self.almanac(words) + elif 25 == page: + s += " A/S flags" + else: + s += " Reserved" + + return s + + def rxm_svsi(self, buf): + """UBX-RXM-SVSI decode, SV Status Info""" + m_len = len(buf) + + u = struct.unpack_from('> 2) & 0x03 + if 0 == raim: + s += "RAIM not available" + elif 1 == raim: + s += "RAIM not active" + elif 2 == raim: + s += "RAIM active" + else: + s += "RAIM ??" + return s + + tim_vrfy_flags = { + 0: "no time aiding done", + 2: "source was RTC", + 3: "source was AID-IN", + } + + def tim_vrfy(self, buf): + """UBX-TIM-VRFY decode, Sourced Time Verification""" + + u = struct.unpack_from(' m_len: + return " Bad Length %s" % m_len + + u = struct.unpack_from(' m_len: + s += " Bad Length %s" % m_len + elif 2 == u[0]: + # Backup File Creation Acknowledge + u1 = struct.unpack_from(' or + # Done, got a full message + if gps.polystr('{"class":"ERROR"') in comment: + # always print gpsd errors + print(comment) + elif VERB_DECODE <= opts['verbosity']: + print(comment) + return consumed + + # else: + comment += chr(c) + continue + + if 'NMEA' == state: + # getting NMEA payload + if (ord('\n') == c) or (ord('\r') == c): + # CR or LF, done, got a full message + # terminates messages on or + if VERB_DECODE <= opts['verbosity']: + print(comment + '\n') + return consumed + + # else: + comment += chr(c) + continue + + if 'RTCM3_1' == state: + # high 6 bits must be zero, + if 0 != (c & 0xfc): + state = 'BASE' + else: + # low 2 bits are MSB of a 10-bit length + m_len = c << 8 + state = 'RTCM3_2' + m_raw.extend([c]) + continue + + if 'RTCM3_2' == state: + # 8 bits are LSB of a 10-bit length + m_len |= 0xff & c + # add 3 for checksum + m_len += 3 + state = 'RTCM3_PAYLOAD' + m_raw.extend([c]) + continue + + if 'RTCM3_PAYLOAD' == state: + m_len -= 1 + m_raw.extend([c]) + m_payload.extend([c]) + if 0 == m_len: + state = 'BASE' + ptype = m_payload[0] << 4 + ptype |= 0x0f & (m_payload[1] >> 4) + if VERB_DECODE <= opts['verbosity']: + print("RTCM3 packet: type %d\n" % ptype) + continue + + if ord('b') == c and 'HEADER1' == state: + # got header 2 + state = 'HEADER2' + continue + + if 'HEADER2' == state: + # got class + state = 'CLASS' + m_class = c + m_raw.extend([c]) + continue + + if 'CLASS' == state: + # got ID + state = 'ID' + m_id = c + m_raw.extend([c]) + continue + + if 'ID' == state: + # got first length + state = 'LEN1' + m_len = c + m_raw.extend([c]) + continue + + if 'LEN1' == state: + # got second length + m_raw.extend([c]) + m_len += 256 * c + if 0 == m_len: + # no payload + state = 'CSUM1' + else: + state = 'PAYLOAD' + continue + + if 'PAYLOAD' == state: + # getting payload + m_raw.extend([c]) + m_payload.extend([c]) + if len(m_payload) == m_len: + state = 'CSUM1' + continue + + if 'CSUM1' == state: + # got ck_a + state = 'CSUM2' + m_ck_a = c + continue + + if 'CSUM2' == state: + # ck_b + state = 'BASE' + m_ck_b = c + # check checksum + chk = self.checksum(m_raw, len(m_raw)) + if (chk[0] != m_ck_a) or (chk[1] != m_ck_b): + sys.stderr.write("%s: ERROR checksum failed," + "was (%d,%d) s/b (%d, %d)\n" % + (PROG_NAME, m_ck_a, m_ck_b, + chk[0], chk[1])) + + s_payload = ''.join('{:02x} '.format(x) for x in m_payload) + x_payload = ','.join(['%02x' % x for x in m_payload]) + + if m_class in self.classes: + this_class = self.classes[m_class] + if 'ids' in this_class: + if m_id in this_class['ids']: + # got an entry for this message + # name is mandatory + s_payload = this_class['ids'][m_id]['name'] + s_payload += ':\n' + + if ((('minlen' in this_class['ids'][m_id]) and + (0 == m_len) and + (0 != this_class['ids'][m_id]['minlen']))): + s_payload += " Poll request" + elif (('minlen' in this_class['ids'][m_id]) and + (this_class['ids'][m_id]['minlen'] > + m_len)): + # failed minimum length for this message + s_payload += " Bad Length %s" % m_len + elif 'dec' in this_class['ids'][m_id]: + # got a decoder for this message + dec = this_class['ids'][m_id]['dec'] + s_payload += dec(self, m_payload) + else: + s_payload += (" len %#x, raw %s" % + (m_len, x_payload)) + + if not s_payload: + # huh? + s_payload = ("%s, len %#x, raw %s" % + (self.class_id_s(m_class, m_id), + m_len, x_payload)) + + if VERB_INFO <= opts['verbosity']: + print("%s, len: %#x" % + (self.class_id_s(m_class, m_id), m_len)) + print("payload: %s" % x_payload) + print("%s\n" % s_payload) + return consumed + + # give up + state = 'BASE' + + # fell out of loop, no more chars to look at + return 0 + + def checksum(self, msg, m_len): + """Calculate u-blox message checksum""" + # the checksum is calculated over the Message, starting and including + # the CLASS field, up until, but excluding, the Checksum Field: + + ck_a = 0 + ck_b = 0 + for c in msg[0:m_len]: + ck_a += c + ck_b += ck_a + + return [ck_a & 0xff, ck_b & 0xff] + + def make_pkt(self, m_class, m_id, m_data): + """Make a message packet""" + # always little endian, leader, class, id, length + m_len = len(m_data) + + # build core message + msg = bytearray(m_len + 6) + struct.pack_into(' opts['protver']: + # UBX-NAV-SOL is ECEF. deprecated in protver 14, gone in protver 27 + m_data = bytearray([0x01, 0x06, rate]) + gps_model.gps_send(6, 1, m_data) + else: + # UBX-NAV-PVT + m_data = bytearray([0x01, 0x07, rate]) + gps_model.gps_send(6, 1, m_data) + # UBX-NAV-SOL is deprecated in protver 14, kill it always + m_data = bytearray([0x01, 0x06, 0]) + gps_model.gps_send(6, 1, m_data) + + # UBX-NAV-POSECEF + m_data = bytearray([0x01, 0x01, rate]) + gps_model.gps_send(6, 1, m_data) + + # UBX-NAV-VELECEF + m_data = bytearray([0x01, 0x11, rate]) + gps_model.gps_send(6, 1, m_data) + + # UBX-NAV-TIMEGPS + # Note: UTC may, or may not be UBX-NAV-TIMEGPS. + # depending on UBX-CFG-NAV5 utcStandard + # Note: We use TIMEGPS to get the leapS + m_data = bytearray([0x01, 0x20, rate]) + gps_model.gps_send(6, 1, m_data) + + # no point doing UBX-NAV-SBAS and UBX-NAV-SVINFO + # faster than every 10 seconds + if rate: + rate_s = 10 + else: + rate_s = 0 + + if 27 > opts['protver']: + # UBX-NAV-SBAS, gone in protver 27 + m_data = bytearray([0x01, 0x32, rate_s]) + gps_model.gps_send(6, 1, m_data) + + # get Satellite Information + if 15 > opts['protver']: + # UBX-NAV-SVINFO - deprecated in protver 15, gone in 27 + m_data = bytearray([0x01, 0x30, rate_s]) + gps_model.gps_send(6, 1, m_data) + + # UBX-NAV-SAT turn it off, if we can + m_data = bytearray([0x01, 0x35, 0]) + gps_model.gps_send(6, 1, m_data) + else: + # use UBX-NAV-SAT for protver 15 and up + m_data = bytearray([0x01, 0x35, rate_s]) + gps_model.gps_send(6, 1, m_data) + + if 27 > opts['protver']: + # UBX-NAV-SVINFO turn it off, if we can + m_data = bytearray([0x01, 0x30, 0]) + gps_model.gps_send(6, 1, m_data) + + if 18 <= opts['protver']: + # first in u-blox 8 + # UBX-NAV-EOE, end of epoch. Good cycle ender + m_data = bytearray([0x01, 0x61, rate]) + gps_model.gps_send(6, 1, m_data) + + if not able and 15 <= opts['protver']: + # if disable, turn off UBX-NAV-VELNED too + m_data = bytearray([0x01, 0x12, 0]) + gps_model.gps_send(6, 1, m_data) + + def send_able_ecef(self, able): + """Enable ECEF messages""" + # set NAV-POSECEF rate + gps_model.send_cfg_msg(1, 1, able) + # set NAV-VELECEF rate + gps_model.send_cfg_msg(1, 0x11, able) + + def send_able_gps(self, able): + """dis/enable GPS/QZSS""" + # GPS and QZSS both on, or both off, together + # GPS + gps_model.send_cfg_gnss1(0, able) + # QZSS + gps_model.send_cfg_gnss1(5, able) + + def send_able_galileo(self, able): + """dis/enable GALILEO""" + gps_model.send_cfg_gnss1(2, able) + + def send_able_glonass(self, able): + """dis/enable GLONASS""" + # Two frequency GPS use BeiDou or GLONASS + # disable, then enable + gps_model.send_cfg_gnss1(6, able) + + def send_able_logfilter(self, able): + """Enable logging""" + + if able: + m_data = bytearray([1, # version + 5, # flags + # All zeros below == log all + 0, 0, # minInterval + 0, 0, # timeThreshold + 0, 0, # speedThreshold + 0, 0, 0, 0 # positionThreshold + ]) + else: + m_data = bytearray([1, # version + 0, # flags + 0, 0, # minInterval + 0, 0, # timeThreshold + 0, 0, # speedThreshold + 0, 0, 0, 0 # positionThreshold + ]) + + # set UBX-CFG-LOGFILTER + gps_model.gps_send(6, 0x47, m_data) + + def send_able_ned(self, able): + """Enable NAV-RELPOSNED and VELNED messages. +protver 15+ required for VELNED +protver 20+, and HP GNSS, required for RELPOSNED""" + if 15 > opts['protver']: + sys.stderr.write('%s: WARNING: protver %d too low for NED\n' % + (PROG_NAME, opts['protver'])) + return + + # set NAV-VELNED rate + gps_model.send_cfg_msg(1, 0x12, able) + + if 20 > opts['protver']: + sys.stderr.write('%s: WARNING: protver %d too low for ' + 'RELPOSNED\n' % + (PROG_NAME, opts['protver'])) + return + + # set NAV-RELPOSNED rate + gps_model.send_cfg_msg(1, 0x3C, able) + + def send_able_nmea(self, able): + """dis/enable basic NMEA messages""" + + # FIXME: does not change UBX-CFG-PRT outProtoMask for current port. + # Workarouund: gpsctl -n + rate = 1 if able else 0 + + # xxGBS + m_data = bytearray([0xf0, 0x09, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxGGA + m_data = bytearray([0xf0, 0x00, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxGGL + m_data = bytearray([0xf0, 0x01, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxGSA + m_data = bytearray([0xf0, 0x02, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxGST + m_data = bytearray([0xf0, 0x07, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxGSV + m_data = bytearray([0xf0, 0x03, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxRMC + m_data = bytearray([0xf0, 0x04, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxVTG + m_data = bytearray([0xf0, 0x05, rate]) + gps_model.gps_send(6, 1, m_data) + + # xxZDA + m_data = bytearray([0xf0, 0x08, rate]) + gps_model.gps_send(6, 1, m_data) + + def send_able_rawx(self, able): + """dis/enable UBX-RXM-RAW/RAWXX""" + + rate = 1 if able else 0 + if 15 > opts['protver']: + # u-blox 7 or earlier, use RAW + sid = 0x10 + else: + # u-blox 8 or later, use RAWX + sid = 0x15 + m_data = bytearray([0x2, sid, rate]) + gps_model.gps_send(6, 1, m_data) + + def send_able_pps(self, able): + """dis/enable PPS, using UBX-CFG-TP5""" + + m_data = bytearray(32) + m_data[0] = 0 # tpIdx + m_data[1] = 1 # version + m_data[2] = 0 # reserved + m_data[3] = 0 # reserved + m_data[4] = 2 # antCableDelay + m_data[5] = 0 # antCableDelay + m_data[6] = 0 # rfGroupDelay + m_data[7] = 0 # rfGroupDelay + m_data[8] = 0x40 # freqPeriod + m_data[9] = 0x42 # freqPeriod + m_data[10] = 0x0f # freqPeriod + m_data[11] = 0 # freqPeriod + m_data[12] = 0x40 # freqPeriodLock + m_data[13] = 0x42 # freqPeriodLock + m_data[14] = 0x0f # freqPeriodLock + m_data[15] = 0 # freqPeriodLock + m_data[16] = 0 # pulseLenRatio + m_data[17] = 0 # pulseLenRatio + m_data[18] = 0 # pulseLenRatio + m_data[19] = 0 # pulseLenRatio + m_data[20] = 0xa0 # pulseLenRatioLock + m_data[21] = 0x86 # pulseLenRatioLock + m_data[22] = 0x1 # pulseLenRatioLock + m_data[23] = 0 # pulseLenRatioLock + m_data[24] = 0 # userConfigDelay + m_data[25] = 0 # userConfigDelay + m_data[26] = 0 # userConfigDelay + m_data[27] = 0 # userConfigDelay + m_data[28] = 0x77 # flags + m_data[29] = 0 # flags + m_data[30] = 0 # flags + m_data[31] = 0 # flags + if not able: + m_data[28] &= ~1 # bit 0 is active + + gps_model.gps_send(6, 0x31, m_data) + + def send_able_sbas(self, able): + """dis/enable SBAS""" + gps_model.send_cfg_gnss1(1, able) + + def send_able_sfrbx(self, able): + """dis/enable UBX-RXM-SFRB/SFRBX""" + + rate = 1 if able else 0 + if 15 > opts['protver']: + # u-blox 7 or earlier, use SFRB + sid = 0x11 + else: + # u-blox 8 or later, use SFRBX + sid = 0x13 + m_data = bytearray([0x2, sid, rate]) + gps_model.gps_send(6, 1, m_data) + + def send_able_tmode2(self, able): + """SURVEYIN, UBX-CFG-TMODE2, set time mode 2 config""" + + m_data = bytearray(28) + if able: + # enable survey-in + m_data[0] = 1 + + # on a NEO-M8T, with good antenna + # five minutes, gets about 1 m + # ten minutes, gets about 0.9 m + # twenty minutes, gets about 0.7 m + # one hour, gets about 0.5 m + # twelve hours, gets about 0.14 m + + # Survey-in minimum duration seconds + seconds = 300 + m_data[20] = seconds & 0x0ff + seconds >>= 8 + m_data[21] = seconds & 0x0ff + seconds >>= 8 + m_data[22] = seconds & 0x0ff + seconds >>= 8 + m_data[23] = seconds & 0x0ff + + # Survey-in position accuracy limit in mm + # make it big, so the duration decides when to end survey + mmeters = 50000 + m_data[24] = mmeters & 0x0ff + mmeters >>= 8 + m_data[25] = mmeters & 0x0ff + mmeters >>= 8 + m_data[26] = seconds & 0x0ff + seconds >>= 8 + m_data[27] = mmeters & 0x0ff + gps_model.gps_send(6, 0x3d, m_data) + + def send_able_tp(self, able): + """dis/enable UBX-TIM-TP Time Pulse""" + rate = 1 if able else 0 + m_data = bytearray([0xd, 0x1, rate]) + gps_model.gps_send(6, 1, m_data) + + def send_cfg_cfg(self, save_clear): + """UBX-CFG-CFG, save config""" + + # Save: save_clear = 0 + # Clear: save_clear = 1 + + # basic configs always available to change: + # ioPort = 1, msgConf = 2, infMsg = 4, navConf = 8, rxmConf =0x10 + cfg1 = 0x1f + # senConf = 1, rinvConf = 2, antConf = 4, logConf = 8, ftsConf = 0x10 + cfg2 = 0x0f + + m_data = bytearray(13) + + # clear mask + # as of protver 27, any bit in clearMask clears all + if 0 == save_clear: + # saving, so do not clear + m_data[0] = 0 + m_data[1] = 0 + else: + # clearing + m_data[0] = cfg1 + m_data[1] = cfg2 + m_data[2] = 0 # + m_data[3] = 0 # + + # save mask + # as of protver 27, any bit in saveMask saves all + if 0 == save_clear: + # saving + m_data[4] = cfg1 + m_data[5] = cfg2 + else: + # clearing, so do not save + m_data[4] = 0 + m_data[5] = 0 + m_data[6] = 0 # + m_data[7] = 0 # + + # load mask + # as of protver 27, any bit in loadMask loads all + if False and 0 == save_clear: + # saving + m_data[8] = 0 + m_data[9] = 0 + else: + # clearing, load it to save a reboot + m_data[8] = cfg1 + m_data[9] = cfg2 + m_data[10] = 0 # + m_data[11] = 0 # + + # deviceMask, where to save it, try all options + # devBBR = 1, devFLASH = 2, devEEPROM = 4, devSpiFlash = 0x10 + m_data[12] = 0x17 + + gps_model.gps_send(6, 0x9, m_data) + + def send_cfg_gnss1(self, gnssId, enable): + """UBX-CFG-GNSS, set GNSS config + +WARNING: the receiver will ACK, then ignore, many seemingly valid settings. +Always double check with "-p CFG-GNSS". +""" + + # FIXME! Add warning if ,2 is requested on single frequency devices + # Only u-blox 9 supports L2, except for M9N does not. + + m_data = bytearray(12) + m_data[0] = 0 # version 0, msgVer + m_data[1] = 0 # read only protVer 23+, numTrkChHw + m_data[2] = 0xFF # read only protVer 23+, numTrkChUse + m_data[3] = 1 # 1 block follows + # block 1 + m_data[4] = gnssId # gnssId + + # m_data[5], resTrkCh, read only protVer 23+ + # m_data[6], maxTrkCh, read only protVer 23+ + if 0 == gnssId: + # GPS + m_data[5] = 8 # resTrkCh + m_data[6] = 16 # maxTrkCh + if 1 == gnssId: + # SBAS + m_data[5] = 1 # resTrkCh + m_data[6] = 3 # maxTrkCh + if 2 == gnssId: + # GALILEO + m_data[5] = 4 # resTrkCh + m_data[6] = 8 # maxTrkCh + if 3 == gnssId: + # BeiDou + m_data[5] = 2 # resTrkCh + m_data[6] = 16 # maxTrkCh + if 4 == gnssId: + # IMES + m_data[5] = 0 # resTrkCh + m_data[6] = 8 # maxTrkCh + if 5 == gnssId: + # QZSS + m_data[5] = 0 # resTrkCh + m_data[6] = 3 # maxTrkCh + if 6 == gnssId: + # GLONASS + m_data[5] = 8 # resTrkCh + m_data[6] = 14 # maxTrkCh + m_data[7] = 0 # reserved1 + m_data[8] = enable # flags + m_data[9] = 0 # flags, unused + + # m_data[10], sigCfgMask, enable all signals + if 0 == gnssId: + # GPS. disable does not seem to work on NEO-M9N + if '2' == opts['parm1'] and enable: + # The NEO-M9N ACKS, then ignores if 0x11 is sent + m_data[10] = 0x11 # flags L1C/A, L2C + else: + m_data[10] = 0x01 # flags L1C/A + elif 1 == gnssId: + # SBAS + m_data[10] = 1 # flags L1C/A + elif 2 == gnssId: + # Galileo + if '2' == opts['parm1'] and enable: + # The NEO-M9N ACKS, then ignores if 0x11 is sent + m_data[10] = 0x21 # flags E1, E5b + else: + m_data[10] = 0x01 # flags E1 + elif 3 == gnssId: + # BeiDou + if '2' == opts['parm1'] and enable: + # The NEO-M9N ACKS, then ignores if 0x11 is sent + m_data[10] = 0x11 # flags B1I, B2I + else: + m_data[10] = 0x01 # flags B1I + elif 4 == gnssId: + # IMES + m_data[10] = 1 # flags L1 + elif 5 == gnssId: + # QZSS + if '2' == opts['parm1'] and enable: + # The NEO-M9N ACKS, then ignores if 0x11 is sent + m_data[10] = 0x15 # flags L1C/A, L1S, L2C + else: + m_data[10] = 0x05 # flags L1C/A, L1S + elif 6 == gnssId: + # GLONASS + if '2' == opts['parm1'] and enable: + # The NEO-M9N ACKS, then ignores if 0x11 is sent + m_data[10] = 0x11 # flags L1, L2 + else: + m_data[10] = 0x01 # flags L1 + else: + # what else? + m_data[10] = 1 # flags L1 + + m_data[11] = 0 # flags, bits 24:31, unused + gps_model.gps_send(6, 0x3e, m_data) + + def poll_cfg_inf(self): + """UBX-CFG-INF, poll""" + + m_data = bytearray(1) + m_data[0] = 0 # UBX + gps_model.gps_send(6, 0x02, m_data) + + m_data[0] = 1 # NMEA + gps_model.gps_send(6, 0x02, m_data) + + def send_cfg_nav5_model(self): + """UBX-CFG-NAV5, set dynamic platform model""" + + m_data = bytearray(36) + m_data[0] = 1 # just setting dynamic model + m_data[1] = 0 # just setting dynamic model + m_data[2] = opts["mode"] + + gps_model.gps_send(6, 0x24, m_data) + + def send_cfg_msg(self, m_class, m_id, rate=None): + """UBX-CFG-MSG, poll, or set, message rates decode""" + m_data = bytearray(2) + m_data[0] = m_class + m_data[1] = m_id + if rate is not None: + m_data.extend([rate]) + gps_model.gps_send(6, 1, m_data) + + def send_cfg_pms(self): + """UBX-CFG-PMS, poll/set Power Management Settings""" + + if opts["mode"] is not None: + m_data = bytearray(8) + # set powerSetupValue to mode + m_data[1] = opts["mode"] + # leave period and onTime zero, which breaks powerSetupValue = 3 + else: + m_data = bytearray(0) + + gps_model.gps_send(6, 0x86, m_data) + + def send_cfg_prt(self): + """UBX-CFG-PRT, get I/O Port settings""" + port = opts['port'] + if port is None: + m_data = bytearray() + else: + m_data = bytearray([port]) + gps_model.gps_send(6, 0x0, m_data) + + def send_set_speed(self, speed): + """"UBX-CFG-PRT, set port""" + port = opts['port'] + # FIXME! Determine and use current port as default + if port is None: + port = 1 # Default to port 1 (UART/UART_1) + if port not in set([1, 2]): + sys.stderr.write('%s: Invalid UART port - %d\n' % + (PROG_NAME, port)) + sys.exit(2) + + # FIXME! Poll current masks, then adjust speed + m_data = bytearray(20) + m_data[0] = port + m_data[4] = 0xc0 # 8N1 + m_data[5] = 0x8 # 8N1 + + m_data[8] = speed & 0xff + m_data[9] = (speed >> 8) & 0xff + m_data[10] = (speed >> 16) & 0xff + m_data[11] = (speed >> 24) & 0xff + + m_data[12] = 3 # in, ubx and nmea + m_data[14] = 3 # out, ubx and nmea + gps_model.gps_send(6, 0, m_data) + + def send_cfg_rst(self, reset_type): + """UBX-CFG-RST, reset""" + # Always do a hardware reset + # If on native USB: both Hardware reset (0) and Software reset (1) + # will disconnect and reconnect, giving you a new /dev/tty. + m_data = bytearray(4) + m_data[0] = reset_type & 0xff + m_data[1] = (reset_type >> 8) & 0xff + gps_model.gps_send(6, 0x4, m_data) + + def send_cfg_tp5(self): + """UBX-CFG-TP5, get time0 decodes, timepulse 0 and 1""" + m_data = bytearray(0) + gps_model.gps_send(6, 0x31, m_data) + # and timepulse 1 + m_data = bytearray(1) + m_data[0] = 1 + gps_model.gps_send(6, 0x31, m_data) + + def send_cfg_valdel(self, keys): + """UBX-CFG-VALDEL, delete config items by key""" + m_data = bytearray(4) + m_data[0] = 0 # version, 0 = transactionless, 1 = transaction + m_data[1] = 6 # 2 = BBR, 4 = flash + # can not delete RAM layer! + # so options stay set until reset! + + for key in keys: + k_data = bytearray(4) + k_data[0] = (key) & 0xff + k_data[1] = (key >> 8) & 0xff + k_data[2] = (key >> 16) & 0xff + k_data[3] = (key >> 24) & 0xff + m_data.extend(k_data) + gps_model.gps_send(0x06, 0x8c, m_data) + + def send_cfg_valget(self, keys): + """UBX-CFG-VALGET, get config items by key""" + m_data = bytearray(4) + m_data[0] = 0 # version, 0 = request, 1 = answer + m_data[1] = 0 # RAM layer + for key in keys: + k_data = bytearray(4) + k_data[0] = (key) & 0xff + k_data[1] = (key >> 8) & 0xff + k_data[2] = (key >> 16) & 0xff + k_data[3] = (key >> 24) & 0xff + m_data.extend(k_data) + # blast them for now, should do one at a time... + layers = {0, 1, 2, 7} + for layer in layers: + m_data[1] = layer + gps_model.gps_send(0x06, 0x8b, m_data) + + def send_cfg_valset(self, nvs): + """UBX-CFG-VALSET, set config items by key/val pairs""" + + m_data = bytearray(4) + m_data[0] = 0 # version, 0 = request, 1 = transaction + m_data[1] = 0x7 # RAM layer, 1=RAM, 2=BBR, 4=Flash + + for nv in nvs: + size = 4 + nv_split = nv.split(',') + name = nv_split[0] + val = nv_split[1] + if 2 < len(nv_split): + m_data[1] = int(nv_split[2]) + print("SNARD: layer %u" % m_data[1]) + + item = gps_model.cfg_by_name(name) + key = item[1] + val_type = item[2] + + cfg_type = self.item_to_type(item) + + size = 4 + cfg_type[0] + frmat = cfg_type[1] + flavor = cfg_type[2] + if 'u' == flavor: + val1 = int(val) + elif 'i' == flavor: + val1 = int(val) + elif 'f' == flavor: + val1 = float(val) + + kv_data = bytearray(size) + kv_data[0] = (key) & 0xff + kv_data[1] = (key >> 8) & 0xff + kv_data[2] = (key >> 16) & 0xff + kv_data[3] = (key >> 24) & 0xff + + struct.pack_into(frmat, kv_data, 4, val1) + m_data.extend(kv_data) + gps_model.gps_send(0x06, 0x8a, m_data) + + def send_poll(self, m_data): + """generic send poll request""" + gps_model.gps_send(m_data[0], m_data[1], m_data[2:]) + + CFG_ANT = [0x06, 0x13] + CFG_BATCH = [0x06, 0x93] + CFG_DAT = [0x06, 0x06] + CFG_GNSS = [0x06, 0x3e] + CFG_GEOFENCE = [0x06, 0x69] + CFG_INF_0 = [0x06, 0x02, 0] + CFG_INF_1 = [0x06, 0x02, 1] + CFG_LOGFILTER = [0x06, 0x47] + CFG_ODO = [0x06, 0x1e] + CFG_PRT = [0x06, 0x00] # current port only + CFG_NAV5 = [0x06, 0x24] + CFG_NAVX5 = [0x06, 0x23] + CFG_NMEA = [0x06, 0x17] + CFG_PM2 = [0x06, 0x3b] + CFG_PMS = [0x06, 0x86] + CFG_RATE = [0x06, 0x08] + CFG_RXM = [0x06, 0x11] + CFG_TP5 = [0x06, 0x31] + CFG_USB = [0x06, 0x1b] + LOG_INFO = [0x21, 0x08] + MON_COMMS = [0x0a, 0x36] + MON_GNSS = [0x0a, 0x28] + MON_HW = [0x0a, 0x09] + MON_HW2 = [0x0a, 0x0b] + MON_HW3 = [0x0a, 0x37] + MON_IO = [0x0a, 0x02] + MON_MSGPP = [0x0a, 0x06] + MON_RF = [0x0a, 0x38] + MON_RXBUF = [0x0a, 0x0a] + MON_TXBUF = [0x0a, 0x08] + MON_VER = [0x0a, 0x04] + NAV_SVIN = [0x01, 0x3b] + TIM_SVIN = [0x0d, 0x04] + + def get_config(self): + """CONFIG. Get a bunch of config messages""" + + cmds = [ubx.MON_VER, # UBX-MON-VER + ubx.CFG_ANT, # UBX-CFG-ANT + ubx.CFG_DAT, # UBX-CFG-DAT + # skip UBX-CFG-DGNSS, HP only + # skip UBX-CFG-DOSC, FTS only + # skip UBX-CFG-ESRC, FTS only + ubx.CFG_GEOFENCE, # UBX-CFG-GEOFENCE + ubx.CFG_GNSS, # UBX-CFG-GNSS + # skip UBX-CFG-HNR, ADR, UDR, only + ubx.CFG_INF_0, # UBX-CFG-INF + ubx.CFG_INF_1, + # skip UBX-CFG-ITFM + ubx.CFG_LOGFILTER, # UBX-CFG-LOGFILTER + ubx.CFG_NAV5, # UBX-CFG-NAV5 + ubx.CFG_NAVX5, # UBX-CFG-NAVX5 + ubx.CFG_NMEA, # UBX-CFG-NMEA + ubx.CFG_ODO, # UBX-CFG-ODO + ubx.CFG_PRT, # UBX-CFG-PRT + ubx.CFG_PM2, # UBX-CFG-PM2 + ubx.CFG_PMS, # UBX-CFG-PMS + ubx.CFG_RATE, # UBX-CFG-RATE + ubx.CFG_RXM, # UBX-CFG-RXM + # skip UBX-CFG-TMODE3, HP only + ubx.CFG_TP5, # UBX-CFG-TP5 + ubx.CFG_USB, # UBX-CFG-USB + ] + + if 22 < opts['protver']: + cmds.append(ubx.CFG_BATCH) # UBX-CFG-BATCH, protVer 23.01+ + + # blast them for now, should do one at a time... + for cmd in cmds: + gps_model.send_poll(cmd) + + def get_status(self): + """STATUS. Get a bunch of status messages""" + + cmds = [ubx.MON_VER, # UBX-MON-VER + ubx.LOG_INFO, # UBX-LOG-INFO + ubx.MON_GNSS, # UBX-MON-GNSS + # UBX-MON-PATH, skipping + ] + + if 27 <= opts['protver']: + cmds.extend([ubx.MON_COMMS, # UBX-MON-COMMS + ubx.MON_HW3, # UBX-MON-HW3 + ]) + else: + # deprecated in 27+ + cmds.extend([ubx.MON_HW, # UBX-MON-HW + ubx.MON_HW2, # UBX-MON-HW2 + ubx.MON_IO, # UBX-MON-IO + ubx.MON_MSGPP, # UBX-MON-MSGPP + ubx.MON_RF, # UBX-MON-RF + ubx.MON_RXBUF, # UBX-MON-RXBUF + ubx.MON_TXBUF, # UBX-MON-TXBUF + ]) + + # should only send these for Time or HP products + cmds.extend([ubx.NAV_SVIN, # UBX-NAV-SVIN + ubx.TIM_SVIN, # UBX-TIM-SVIN + ]) + + # blast them for now, should do one at a time... + for cmd in cmds: + gps_model.send_poll(cmd) + + able_commands = { + # en/dis able BeiDou + "BEIDOU": {"command": send_able_beidou, + "help": "BeiDou B1. BEIDOU,5 for B1 and B2"}, + # en/dis able basic binary messages + "BINARY": {"command": send_able_binary, + "help": "basic binary messages"}, + # en/dis able ECEF + "ECEF": {"command": send_able_ecef, + "help": "ECEF"}, + # en/dis able GPS + "GPS": {"command": send_able_gps, + "help": "GPS and QZSS L1C/A. GPS,2 for L1C/A and L2C"}, + # en/dis able GALILEO + "GALILEO": {"command": send_able_galileo, + "help": "GALILEO E1. GALILEO,2 for E1 and E5b"}, + # en/dis able GLONASS + "GLONASS": {"command": send_able_glonass, + "help": "GLONASS L1. GLONASS,2 for L1 and L2"}, + # en/dis able LOG + "LOG": {"command": send_able_logfilter, + "help": "Data Logger"}, + # en/dis able NED + "NED": {"command": send_able_ned, + "help": "NAV-VELNED and NAV-RELPOSNED"}, + # en/dis able basic NMEA messages + "NMEA": {"command": send_able_nmea, + "help": "basic NMEA messages"}, + # en/dis able RAW/RAWX + "RAWX": {"command": send_able_rawx, + "help": "RAW/RAWX measurements"}, + # en/dis able PPS + "PPS": {"command": send_able_pps, + "help": "PPS on TIMPULSE"}, + # en/dis able SBAS + "SBAS": {"command": send_able_sbas, + "help": "SBAS L1C"}, + # en/dis able SFRB/SFRBX + "SFRBX": {"command": send_able_sfrbx, + "help": "SFRB/SFRBX subframes"}, + # en/dis able TP time pulse message + "TP": {"command": send_able_tp, + "help": "TP Time Pulse message"}, + # en/dis able TMODE2 Survey-in + "SURVEYIN": {"command": send_able_tmode2, + "help": "Survey-in mode with TMODE2"}, + } + commands = { + # UBX-CFG-RST + "COLDBOOT": {"command": send_cfg_rst, + "help": "UBS-CFG-RST coldboot the GPS", + "opt": 0xffff}, + # CONFIG + "CONFIG": {"command": get_config, + "help": "Get a lot of receiver config"}, + # UBX-CFG-RST + "HOTBOOT": {"command": send_cfg_rst, + "help": "UBX-CFG-RST hotboot the GPS", + "opt": 0}, + # UBX-CFG-NAV5 + "MODEL": {"command": send_cfg_nav5_model, + "help": "set UBX-CFG-NAV5 Dynamic Platform Model"}, + # UBX-CFG-PMS + "PMS": {"command": send_cfg_pms, + "help": "set UBX-CFG-PMS power management settings"}, + # UBX-CFG-CFG + "RESET": {"command": send_cfg_cfg, + "help": "UBX-CFG-CFG reset config to defaults", + "opt": 1}, + # UBX-CFG-CFG + "SAVE": {"command": send_cfg_cfg, + "help": "UBX-CFG-CFG save current config", + "opt": 0}, + # STATUS + "STATUS": {"command": get_status, + "help": "Get a lot of receiver status"}, + # UBX-CFG-RST + "WARMBOOT": {"command": send_cfg_rst, + "help": "UBX-CFG-RST warmboot the GPS", + "opt": 1}, + # UBX-AID-ALM + "AID-ALM": {"command": send_poll, "opt": [0x0b, 0x30], + "help": "poll UBX-AID-ALM Poll GPS Aiding Almanac Data"}, + # UBX-AID-AOP + "AID-AOP": {"command": send_poll, "opt": [0x0b, 0x33], + "help": "poll UBX-AID-AOP Poll Poll AssistNow " + "Autonomous data"}, + # UBX-AID-DATA + "AID-DATA": {"command": send_poll, "opt": [0x0b, 0x10], + "help": "Poll all GPS Initial Aiding Data"}, + # UBX-AID-EPH + "AID-EPH": {"command": send_poll, "opt": [0x0b, 0x31], + "help": "poll UBX-AID-EPH Poll GPS Aiding Ephemeris Data"}, + # UBX-AID-HUI + "AID-HUI": {"command": send_poll, "opt": [0x0b, 0x02], + "help": "poll UBX-AID-HUI Poll GPS Health, UTC, Iono"}, + # UBX-AID-INI + "AID-INI": {"command": send_poll, "opt": [0x0b, 0x01], + "help": "poll UBX-AID-INI Poll Aiding position, time, " + "frequency, clock drift"}, + # UBX-CFG-ANT + "CFG-ANT": {"command": send_poll, "opt": [0x06, 0x13], + "help": "poll UBX-CFG-ANT antenna config"}, + # UBX-CFG-BATCH + # Assume 23 is close enough to the proper 23.01 + "CFG-BATCH": {"command": send_poll, "opt": [0x06, 0x93], + "help": "poll UBX-CFG-BATCH data batching config", + "minVer": 23}, + # UBX-CFG-DAT + "CFG-DAT": {"command": send_poll, "opt": [0x06, 0x06], + "help": "poll UBX-CFG-DAT Datum Setting"}, + # UBX-CFG-DOSC + "CFG-DOSC": {"command": send_poll, "opt": [0x06, 0x61], + "help": "poll UBX-CFG-DOSC Disciplined oscillator " + "configuration"}, + # UBX-CFG-ESRC + "CFG-ESRC": {"command": send_poll, "opt": [0x06, 0x60], + "help": "poll UBX-CFG-ESRC External synchronization " + "source configuration"}, + # UBX-CFG-FXN + "CFG-FXN": {"command": send_poll, "opt": [0x06, 0x0e], + "help": "poll UBX-CFG-FXN FXN Configuration"}, + # UBX-CFG-GEOFENCE + "CFG-GEOFENCE": {"command": send_poll, "opt": [0x06, 0x69], + "help": "poll UBX-CFG-GEOFENCE Geofencing " + "configuration"}, + # UBX-CFG-GNSS + "CFG-GNSS": {"command": send_poll, "opt": [0x06, 0x3e], + "help": "poll UBX-CFG-GNSS GNSS config"}, + # UBX-CFG-HNR + "CFG-HNR": {"command": send_poll, "opt": [0x06, 0x5c], + "help": "poll UBX-CFG-HNR High Navigation Rate Settings"}, + # UBX-CFG-INF + "CFG-INF": {"command": poll_cfg_inf, + "help": "poll UBX-CFG-INF Information Message " + "Configuration"}, + # UBX-CFG-ITFM + "CFG-ITFM": {"command": send_poll, "opt": [0x06, 0x39], + "help": "poll UBX-CFG-RXM Jamming/Interference " + "Monitor configuration"}, + # UBX-CFG-LOGFILTER + "CFG-LOGFILTER": {"command": send_poll, "opt": [0x06, 0x47], + "help": "poll UBX-CFG-LOGFILTER " + " Data Logger Configuration", + "minVer": 14}, + # UBX-CFG-NAV5 + "CFG-NAV5": {"command": send_poll, "opt": [0x06, 0x24], + "help": "poll UBX-CFG-NAV5 Nav Engines settings"}, + # UBX-CFG-NAVX5 + "CFG-NAVX5": {"command": send_poll, "opt": [0x06, 0x23], + "help": "poll UBX-CFG-NAVX5 Nav Expert Settings"}, + # UBX-CFG-NMEA + "CFG-NMEA": {"command": send_poll, "opt": [0x06, 0x17], + "help": "poll UBX-CFG-NMEA Extended NMEA protocol " + "configuration V1"}, + # UBX-CFG-ODO + "CFG-ODO": {"command": send_poll, "opt": [0x06, 0x1e], + "help": "poll UBX-CFG-ODO Odometer, Low-speed COG " + "Engine Settings"}, + # UBX-CFG-PM + "CFG-PM": {"command": send_poll, "opt": [0x06, 0x32], + "help": "poll UBX-CFG-PM Power management settings"}, + # UBX-CFG-PM2 + "CFG-PM2": {"command": send_poll, "opt": [0x06, 0x3b], + "help": "poll UBX-CFG-PM2 Extended power management " + "settings"}, + # UBX-CFG-PMS + "CFG-PMS": {"command": send_poll, "opt": [0x06, 0x86], + "help": "poll UBX-CFG-PMS power management settings"}, + # UBX-CFG-PRT + "CFG-PRT": {"command": send_cfg_prt, + "help": "poll UBX-CFG-PRT I/O port settings"}, + # UBX-CFG-RATE + "CFG-RATE": {"command": send_poll, "opt": [0x06, 0x08], + "help": "poll UBX-CFG-RATE Navigation/Measurement " + "Rate Settings"}, + # UBX-CFG-RINV + "CFG-RINV": {"command": send_poll, "opt": [0x06, 0x34], + "help": "poll UBX-CFG-RINV Contents of Remote Inventory"}, + # UBX-CFG-RXM + "CFG-RXM": {"command": send_poll, "opt": [0x06, 0x11], + "help": "poll UBX-CFG-RXM RXM configuration"}, + # UBX-CFG-SBAS + "CFG-SBAS": {"command": send_poll, "opt": [0x06, 0x16], + "help": "poll UBX-CFG-SBAS SBAS settings"}, + # UBX-CFG-SLAS + "CFG-SLAS": {"command": send_poll, "opt": [0x06, 0x8d], + "help": "poll UBX-CFG-SLAS SLAS configuration"}, + # UBX-CFG-SMGR + "CFG-SMGR": {"command": send_poll, "opt": [0x06, 0x62], + "help": "poll UBX-CFG-SMGR Synchronization manager " + " configuration"}, + # UBX-CFG-TMODE + "CFG-TMODE": {"command": send_poll, "opt": [0x06, 0x1d], + "help": "poll UBX-CFG-TMODE time mode settings"}, + # UBX-CFG-TMODE2 + "CFG-TMODE2": {"command": send_poll, "opt": [0x06, 0x3d], + "help": "poll UBX-CFG-TMODE2 time mode 2 config"}, + # UBX-CFG-TP + "CFG-TP": {"command": send_poll, "opt": [0x06, 0x07], + "help": "poll UBX-CFG-TP TimePulse Parameters"}, + # UBX-CFG-TP5 + "CFG-TP5": {"command": send_cfg_tp5, + "help": "poll UBX-TIM-TP5 time pulse decodes"}, + # UBX-CFG-USB + "CFG-USB": {"command": send_poll, "opt": [0x06, 0x1b], + "help": "poll UBX-CFG-USB USB config"}, + # UBX-LOG-CREATE + "LOG-CREATE": {"command": send_poll, + "opt": [0x21, 0x07, 0, 1, 0, 0, 0, 0, 0, 0], + "help": "send UBX-LOG-CREATE", + "minVer": 14}, + # UBX-LOG-ERASE + "LOG-ERASE": {"command": send_poll, "opt": [0x21, 0x03], + "help": "send UBX-LOG-ERASE", + "minVer": 14}, + # UBX-LOG-INFO + "LOG-INFO": {"command": send_poll, "opt": [0x21, 0x08], + "help": "poll UBX-LOG-INFO", + "minVer": 14}, + # UBX-LOG-RETRIEVE + "LOG-RETRIEVE": {"command": send_poll, + "opt": [0x21, 9, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0], + "help": "send UBX-LOG-RETRIEVE", + "minVer": 14}, + # UBX-LOG-RETRIEVEBATCH + # Assume 23 is close enough to the proper 23.01 + "LOG-RETRIEVEBATCH": {"command": send_poll, + "opt": [0x21, 0x10, 0, 1, 0, 0], + "help": "send UBX-LOG-RETRIEVEBATCH", + "maxVer": 23.99, + "minVer": 23}, + # UBX-LOG-STRING + "LOG-STRING": {"command": send_poll, + "opt": [0x21, 4, ord("H"), ord("i")], + "help": "send UBX-LOG-STRING", + "minVer": 14}, + # UBX-MGA-DBD + "MGA-DBD": {"command": send_poll, "opt": [0x13, 0x80], + "help": "poll UBX-MGA-DBD Poll the Navigation Database"}, + # UBX-MON-BATCH + # Assume 23 is close enough to the proper 23.01 + "MON-BATCH": {"command": send_poll, "opt": [0x0a, 0x32], + "help": "poll UBX-MON-BATCH Data batching " + "buffer status", + "maxVer": 23.99, + "minVer": 23}, + # UBX-MON-COMMS + "MON-COMMS": {"command": send_poll, "opt": [0x0a, 0x36], + "help": "poll UBX-MON-COMMS Comm port " + "information"}, + # UBX-MON-GNSS + "MON-GNSS": {"command": send_poll, "opt": [0x0a, 0x28], + "help": "poll UBX-MON-GNSS major GNSS selection"}, + # UBX-MON-HW + "MON-HW": {"command": send_poll, "opt": [0x0a, 0x09], + "help": "poll UBX-MON-HW Hardware Status"}, + # UBX-MON-HW2 + "MON-HW2": {"command": send_poll, "opt": [0x0a, 0x0b], + "help": "poll UBX-MON-HW2 Exended Hardware Status"}, + # UBX-MON-HW3 + "MON-HW3": {"command": send_poll, "opt": [0x0a, 0x37], + "help": "poll UBX-MON-HW3 HW I/O pin infromation"}, + # UBX-MON-IO + "MON-IO": {"command": send_poll, "opt": [0x0a, 0x02], + "help": "poll UBX-MON-IO I/O Subsystem Status"}, + # UBX-MON-MSGPP + "MON-MSGPP": {"command": send_poll, "opt": [0x0a, 0x06], + "help": "poll UBX-MON-MSGPP Message Parese and " + "Process Status"}, + # UBX-MON-PATCH + "MON-PATCH": {"command": send_poll, "opt": [0x0a, 0x27], + "help": "poll UBX-MON-PATCH Info on Installed Patches"}, + # UBX-MON-RF + "MON-RF": {"command": send_poll, "opt": [0x0a, 0x38], + "help": "poll UBX-MON-RF RF Information"}, + # UBX-MON-RXBUF + "MON-RXBUF": {"command": send_poll, "opt": [0x0a, 0x07], + "help": "poll UBX-MON-RXBUF Receiver Buffer Status"}, + # UBX-MON-SMGR + "MON-SMGR": {"command": send_poll, "opt": [0x0a, 0x2e], + "help": "poll UBX-MON-SMGR Synchronization manager " + "configuration"}, + # UBX-MON-TXBUF + "MON-TXBUF": {"command": send_poll, "opt": [0x0a, 0x08], + "help": "poll UBX-MON-TXBUF Transmitter Buffer Status"}, + # UBX-MON-VER + "MON-VER": {"command": send_poll, "opt": [0x0a, 0x04], + "help": "poll UBX-MON-VER GPS version"}, + # UBX-NAV-AOPSTATUS + "NAV-AOPSTATUS": {"command": send_poll, "opt": [0x01, 0x60], + "help": "poll UBX-NAV-AOPSTATUS AssistNow " + "Autonomous Status"}, + # UBX-NAV-ATT + "NAV-ATT": {"command": send_poll, "opt": [0x1, 0x5], + "help": "poll UBX-NAV-ATT Attitude Solution"}, + # UBX-NAV-CLOCK + "NAV-CLOCK": {"command": send_poll, "opt": [0x01, 0x22], + "help": "poll UBX-NAV-CLOCK Clock Solution"}, + # UBX-NAV-DGPS + "NAV-DGPS": {"command": send_poll, "opt": [0x01, 0x31], + "help": "poll UBX-NAV-DGPS DGPS Data Used for NAV"}, + # UBX-NAV-DOP + "NAV-DOP": {"command": send_poll, "opt": [0x01, 0x04], + "help": "poll UBX-NAV-DOP Dilution of Precision"}, + # UBX-NAV-GEOFENCE + "NAV-GEOFENCE": {"command": send_poll, "opt": [0x01, 0x39], + "help": "poll UBX-NAV-GEOFENCE Geofence status"}, + # UBX-NAV-HPPOSECEF + "NAV-HPPOSECEF": {"command": send_poll, "opt": [0x01, 0x13], + "help": "poll UBX-NAV-HPPOSECEF ECEF position"}, + # UBX-NAV-HPPOSLLH + "NAV-HPPOSLLH": {"command": send_poll, "opt": [0x01, 0x14], + "help": "poll UBX-NAV-HPPOSECEF LLH position"}, + # UBX-NAV-ODO + "NAV-ODO": {"command": send_poll, "opt": [0x01, 0x09], + "help": "poll UBX-NAV-ODO Odometer Solution"}, + # UBX-NAV-ORB + "NAV-ORB": {"command": send_poll, "opt": [0x01, 0x34], + "help": "poll UBX-NAV-ORB GNSS Orbit Database Info"}, + # UBX-NAV-POSECEF + "NAV-POSECEF": {"command": send_poll, "opt": [0x01, 0x01], + "help": "poll UBX-NAV-POSECEF ECEF position"}, + # UBX-NAV-POSLLH + "NAV-POSLLH": {"command": send_poll, "opt": [0x01, 0x02], + "help": "poll UBX-NAV-POSLLH LLH position"}, + # UBX-NAV-PVT + "NAV-PVT": {"command": send_poll, "opt": [0x01, 0x07], + "help": "poll UBX-NAV-PVT Navigation Position Velocity " + "Time Solution"}, + # UBX-NAV-RELPOSNED + # HP only, 20+, otherwise not ACKed or NACKed + "NAV-RELPOSNED": {"command": send_poll, "opt": [0x01, 0x3c], + "help": "poll UBX-NAV-RELPOSNED Relative " + "Positioning Information in NED frame"}, + # UBX-NAV-RESETODO + "NAV-RESETODO": {"command": send_poll, "opt": [0x01, 0x10], + "help": "UBX-NAV-RESETODO Reset odometer"}, + # UBX-NAV-SAT + "NAV-SAT": {"command": send_poll, "opt": [0x01, 0x35], + "help": "poll UBX-NAV-SAT Satellite Information"}, + # UBX-NAV-SBAS + "NAV-SBAS": {"command": send_poll, "opt": [0x01, 0x32], + "help": "poll UBX-NAV-SBAS SBAS Status Data"}, + # UBX-NAV-SIG + "NAV-SIG": {"command": send_poll, "opt": [0x01, 0x43], + "help": "poll UBX-NAV-SIG Signal Information"}, + # UBX-NAV-SLAS + "NAV-SLAS": {"command": send_poll, "opt": [0x01, 0x42], + "help": "poll UBX-NAV-SLAS QZSS L1S SLAS Status Data"}, + # UBX-NAV-SOL + "NAV-SOL": {"command": send_poll, "opt": [0x01, 0x06], + "help": "poll UBX-NAV-SOL Navigation Solution " + "Information"}, + # UBX-NAV-STATUS + "NAV-STATUS": {"command": send_poll, "opt": [0x01, 0x03], + "help": "poll UBX-NAV-STATUS Receiver Nav Status"}, + # UBX-NAV-SVINFO + "NAV-SVINFO": {"command": send_poll, "opt": [0x01, 0x30], + "help": "poll UBX-NAV-SVINFO Satellite Information"}, + # UBX-NAV-TIMEBDS + "NAV-TIMEBDS": {"command": send_poll, "opt": [0x01, 0x24], + "help": "poll UBX-NAV-TIMEBDS BDS Time Solution"}, + # UBX-NAV-TIMEGAL + "NAV-TIMEGAL": {"command": send_poll, "opt": [0x01, 0x25], + "help": "poll UBX-NAV-TIMEGAL Galileo Time Solution"}, + # UBX-NAV-TIMEGLO + "NAV-TIMEGLO": {"command": send_poll, "opt": [0x01, 0x23], + "help": "poll UBX-NAV-TIMEGLO GLO Time Solution"}, + # UBX-NAV-TIMEGPS + "NAV-TIMEGPS": {"command": send_poll, "opt": [0x01, 0x20], + "help": "poll UBX-NAV-TIMEGPS GPS Time Solution"}, + # UBX-NAV-TIMELS + "NAV-TIMELS": {"command": send_poll, "opt": [0x01, 0x26], + "help": "poll UBX-NAV-TIMELS Leap Second Info"}, + # UBX-NAV-TIMEUTC + "NAV-TIMEUTC": {"command": send_poll, "opt": [0x01, 0x21], + "help": "poll UBX-NAV-TIMEUTC UTC Time Solution"}, + # UBX-NAV-VELECEF + "NAV-VELECEF": {"command": send_poll, "opt": [0x01, 0x11], + "help": "poll UBX-NAV-VELECEF ECEF velocity"}, + # UBX-NAV-VELNED + "NAV-VELNED": {"command": send_poll, "opt": [0x01, 0x12], + "help": "poll UBX-NAV-VELNED NED velocity"}, + # UBX-RXM-IMES + "RXM-IMES": {"command": send_poll, "opt": [0x02, 0x61], + "help": "poll UBX-RXM-IMES Indoor Messaging System " + "Information"}, + # UBX-RXM-MEASX + "RXM-MEASX": {"command": send_poll, "opt": [0x02, 0x14], + "help": "poll UBX-RXM-MEASX Satellite Measurements " + " for RRLP"}, + # UBX-RXM-RAWX + "RXM-RAWX": {"command": send_poll, "opt": [0x02, 0x15], + "help": "poll UBX-RXM-RAWX raw measurement data"}, + # UBX-CFG-SBAS + "SEC-UNIQID": {"command": send_poll, "opt": [0x27, 0x03], + "help": "poll UBX-SEC-UNIQID Unique chip ID"}, + # UBX-TIM-SVIN + "TIM-SVIN": {"command": send_poll, "opt": [0x0d, 0x04], + "help": "poll UBX-TIM-SVIN survey in data"}, + # UBX-TIM-TM2 + "TIM-TM2": {"command": send_poll, "opt": [0x0d, 0x03], + "help": "poll UBX-TIM-TM2 time mark data"}, + # UBX-TIM-TP + "TIM-TP": {"command": send_poll, "opt": [0x0d, 0x01], + "help": "poll UBX-TIM-TP time pulse timedata"}, + # UBX-TIM-VRFY + "TIM-VRFY": {"command": send_poll, "opt": [0x0d, 0x06], + "help": "poll UBX-TIM-VRFY Sourced Time Verification"}, + # UBX-UPD-SOS + "UPD-SOS": {"command": send_poll, "opt": [0x09, 0x14], + "help": "poll UBX-UPD-SOS Backup File restore Status"}, + # UBX-UPD-SOS + "UPD-SOS0": {"command": send_poll, "opt": [0x09, 0x14, 0, 0, 0, 0], + "help": "UBX-UPD-SOS Create Backup File in Flash"}, + # UBX-UPD-SOS + "UPD-SOS1": {"command": send_poll, "opt": [0x09, 0x14, 1, 0, 0, 0], + "help": "UBX-UPD-SOS Create Clear File in Flash"}, + } + # end class ubx + + +class gps_io(object): + """All the GPS I/O in one place" + + Three types of GPS I/O + 1. read only from a file + 2. read/write through a device + 3. read only from a gpsd instance + """ + + out = b'' + ser = None + input_is_device = False + + def __init__(self): + """Initialize class""" + + Serial = serial + Serial_v3 = Serial and Serial.VERSION.split('.')[0] >= '3' + # buffer to hold read data + self.out = b'' + + # open the input: device, file, or gpsd + if opts['input_file_name'] is not None: + # check if input file is a file or device + try: + mode = os.stat(opts['input_file_name']).st_mode + except OSError: + sys.stderr.write('%s: failed to open input file %s\n' % + (PROG_NAME, opts['input_file_name'])) + sys.exit(1) + + if stat.S_ISCHR(mode): + # character device, need not be read only + self.input_is_device = True + + if ((opts['disable'] or opts['enable'] or opts['poll'] or + opts['oaf_name'])): + + # check that we can write + if opts['read_only']: + sys.stderr.write('%s: read-only mode, ' + 'can not send commands\n' % PROG_NAME) + sys.exit(1) + if self.input_is_device is False: + sys.stderr.write('%s: input is plain file, ' + 'can not send commands\n' % PROG_NAME) + sys.exit(1) + + if opts['target']['server'] is not None: + # try to open local/remote gpsd daemon + try: + self.ser = gps.gpscommon(host=opts['target']['server'], + port=opts['target']['port'], + verbose=0) + + # alias self.ser.write() to self.write_gpsd() + self.ser.write = self.write_gpsd + + # ask for raw, not rare, data + data_out = b'?WATCH={' + if opts['target']['device'] is not None: + # add in the requested device + data_out += (b'"device":"' + + gps.polybytes(opts['target']['device']) + + b'",') + data_out += b'"enable":true,"raw":2}\r\n' + if VERB_RAW <= opts['verbosity']: + print("sent: ", data_out) + self.ser.send(data_out) + except socket.error as err: + sys.stderr.write('%s: failed to connect to gpsd %s\n' % + (PROG_NAME, err)) + sys.exit(1) + + elif self.input_is_device: + # configure the serial connections (the parameters refer to + # the device you are connecting to) + + # pyserial Ver 3.0+ changes writeTimeout to write_timeout + # Using the wrong one causes an error + write_timeout_arg = ('write_timeout' + if Serial_v3 else 'writeTimeout') + try: + self.ser = Serial.Serial( + baudrate=opts['input_speed'], + # 8N1 is UBX default + bytesize=Serial.EIGHTBITS, + parity=Serial.PARITY_NONE, + port=opts['input_file_name'], + stopbits=Serial.STOPBITS_ONE, + # read timeout + timeout=0.05, + **{write_timeout_arg: 0.5} + ) + except AttributeError: + sys.stderr.write('%s: failed to import pyserial\n' % PROG_NAME) + sys.exit(2) + except Serial.serialutil.SerialException: + # this exception happens on bad serial port device name + sys.stderr.write('%s: failed to open serial port "%s"\n' + '%s: Your computer has the serial ports:\n' % + (PROG_NAME, opts['input_file_name'], + PROG_NAME)) + + # print out list of supported ports + import serial.tools.list_ports as List_Ports + ports = List_Ports.comports() + for port in ports: + sys.stderr.write(" %s: %s\n" % + (port.device, port.description)) + sys.exit(1) + + # flush input buffer, discarding all its contents + # pyserial 3.0+ deprecates flushInput() in favor of + # reset_input_buffer(), but flushInput() is still present. + self.ser.flushInput() + + else: + # Read from a plain file of UBX messages + try: + self.ser = open(opts['input_file_name'], 'rb') + except IOError: + sys.stderr.write('%s: failed to open input %s\n' % + (PROG_NAME, opts['input_file_name'])) + sys.exit(1) + + def read(self, read_opts): + """Read from device, until timeout or expected message""" + + # are we expecting a certain message? + if gps_model.expect_statement_identifier: + # assume failure, until we see expected message + ret_code = 1 + else: + # not expecting anything, so OK if we did not see it. + ret_code = 0 + + try: + if read_opts['target']['server'] is not None: + # gpsd input + start = gps.monotonic() + while read_opts['input_wait'] > (gps.monotonic() - start): + # First priority is to be sure the input buffer is read. + # This is to prevent input buffer overuns + if 0 < self.ser.waiting(): + # We have serial input waiting, get it + # No timeout possible + # RTCM3 JSON can be over 4.4k long, so go big + new_out = self.ser.sock.recv(8192) + if raw is not None: + # save to raw file + raw.write(new_out) + self.out += new_out + + consumed = gps_model.decode_msg(self.out) + self.out = self.out[consumed:] + if ((gps_model.expect_statement_identifier and + (gps_model.expect_statement_identifier == + gps_model.last_statement_identifier))): + # Got what we were waiting for. Done? + ret_code = 0 + if not read_opts['input_forced_wait']: + # Done + break + + elif self.input_is_device: + # input is a serial device + start = gps.monotonic() + while read_opts['input_wait'] > (gps.monotonic() - start): + # First priority is to be sure the input buffer is read. + # This is to prevent input buffer overuns + # pyserial 3.0+ deprecates inWaiting() in favor of + # in_waiting, but inWaiting() is still present. + if 0 < self.ser.inWaiting(): + # We have serial input waiting, get it + # 1024 is comfortably large, almost always the + # Read timeout is what causes ser.read() to return + new_out = self.ser.read(1024) + if raw is not None: + # save to raw file + raw.write(new_out) + self.out += new_out + + consumed = gps_model.decode_msg(self.out) + self.out = self.out[consumed:] + if ((gps_model.expect_statement_identifier and + (gps_model.expect_statement_identifier == + gps_model.last_statement_identifier))): + # Got what we were waiting for. Done? + ret_code = 0 + if not read_opts['input_forced_wait']: + # Done + break + else: + # ordinary file, so all read at once + self.out += self.ser.read() + if raw is not None: + # save to raw file + raw.write(self.out) + + while True: + consumed = gps_model.decode_msg(self.out) + self.out = self.out[consumed:] + if 0 >= consumed: + break + + except IOError: + # This happens on a good device name, but gpsd already running. + # or if USB device unplugged + sys.stderr.write('%s: failed to read %s\n' + '%s: Is gpsd already holding the port?\n' + % (PROG_NAME, read_opts['input_file_name'], + PROG_NAME)) + return 1 + + if 0 < ret_code: + # did not see the message we were expecting to see + sys.stderr.write('%s: waited %0.2f seconds for, ' + 'but did not get: %%%s%%\n' + % (PROG_NAME, read_opts['input_wait'], + gps_model.expect_statement_identifier)) + return ret_code + + def write_gpsd(self, data): + """write data to gpsd daemon""" + + # HEXDATA_MAX = 512, from gps.h, The max hex digits can write. + # Input data is binary, converting to hex doubles its size. + # Limit binary data to length 255, so hex data length less than 510. + if 255 < len(data): + sys.stderr.write('%s: trying to send %d bytes, max is 255\n' + % (PROG_NAME, len(data))) + return 1 + + if opts['target']['device'] is not None: + # add in the requested device + data_out = (b'?DEVICE={"path":"' + + gps.polybytes(opts['target']['device']) + b'",') + else: + data_out = b'?DEVICE={' + + # Convert binary data to hex and build the message. + data_out += b'"hexdata":"' + binascii.hexlify(data) + b'"}\r\n' + if VERB_RAW <= opts['verbosity']: + print("sent: ", data_out) + self.ser.send(data_out) + return 0 + + +# instantiate the GPS class +gps_model = ubx() + + +def usage(): + """Ouput usage information, and exit""" + print('usage: %s [OPTION] ... [[server[:port[:device]]]]\n\n' + ' Options:\n' + ' -? print help, increase -v for extra help\n' + ' -c C send raw command C (cls,id...) to GPS\n' + ' -d D disable D\n' + ' -e E enable E\n' + ' -f F open F as file/device\n' + ' default: %s\n' + ' -g I get config item I\n' + ' -h print help, increase -v for extra help\n' + ' -i P port (interface ) for UBX-CFG-PRT\n' + ' -m M optional mode to -p P\n' + ' -P P Protocol version for sending commands\n' + ' default: %s\n' + ' -p P send a preset query P to GPS\n' + ' -R R save raw data from GPS in file R\n' + ' default: %s\n' + ' -r open file/device read only\n' + ' -S S set GPS speed to S\n' + ' -s S set port speed to S\n' + ' default: %s bps\n' + ' -V print version\n' + ' -v V Set verbosity level to V, 0 to 4\n' + ' default: %d\n' + ' -w W wait time W before exiting\n' + ' default: %s seconds\n' + ' -x I delete config item I\n' + ' -z I,v,l set config item I to v, in option layer l\n' + '\n' % + (PROG_NAME, opts['input_file_name'], + opts['protver'], opts['raw_file'], + opts['input_speed'], opts['input_wait'], + opts['verbosity']) + ) + + if VERB_DECODE <= opts['verbosity']: + print('D and E can be one of:') + for item in sorted(gps_model.able_commands.keys()): + print(" %-13s %s" % + (item, gps_model.able_commands[item]["help"])) + + print('\nP can be one of:') + for item in sorted(gps_model.commands.keys()): + print(" %-13s %s" % (item, gps_model.commands[item]["help"])) + print('\n') + if VERB_DECODE < opts['verbosity']: + print('\nConfiguration items for -g, -x and -z can be one of:') + for item in sorted(gps_model.cfgs): + print(" %s\n" + " %s" % (item[0], item[5])) + print('\n') + + print('Options can be placed in the UBXOPTS environment variable.\n' + 'UBXOPTS is processed before the CLI options.') + sys.exit(0) + + +if 'UBXOPTS' in os.environ: + # grab the UBXOPTS environment variable for options + opts['progopts'] = os.environ['UBXOPTS'] + options = opts['progopts'].split(' ') + sys.argv[1:] +else: + options = sys.argv[1:] + + +try: + (options, arguments) = getopt.getopt(options, + "?c:d:e:f:g:hi:m:rP:p:" + "s:w:v:R:S:Vx:z:") +except getopt.GetoptError as err: + sys.stderr.write("%s: %s\n" + "Try '%s -h' for more information.\n" % + (PROG_NAME, str(err), PROG_NAME)) + sys.exit(2) + +for (opt, val) in options: + if opt == '-c': + opts['command'] = val + elif opt == '-d': + parts = val.split(',') + # don't force the user to upper case + opts['disable'] = parts[0].upper() + if 1 in parts: + # optional parameter + opts['parm1'] = parts[1] + elif opt == '-e': + parts = val.split(',') + # don't force the user to upper case + opts['enable'] = val.upper() + if 1 in parts: + # optional parameter + opts['parm1'] = parts[1] + elif opt == '-f': + opts['input_file_name'] = val + elif opt == '-g': + opts['get_item'].append(val) + elif opt in ('-h', '-?'): + opts['help'] = True + elif opt == '-i': + valnum = gps_model.port_id_map.get(val.upper()) + opts['port'] = valnum if valnum is not None else int(val) + elif opt == '-m': + opts['mode'] = int(val) + elif opt == '-P': + # to handle vesion like 23.01 + opts['protver'] = float(val) + if 10 > opts['protver']: + opts['protver'] = 10.0 + # Dunno the max protver, NEO-M9N is 32.00 + elif opt == '-p': + # don't force the user to upper case + opts['poll'] = val.upper() + elif opt in '-R': + # raw log file + opts['raw_file'] = val + elif opt == '-r': + opts['read_only'] = True + elif opt in '-S': + opts['set_speed'] = int(val) + if opts['set_speed'] not in gps_model.speeds: + sys.stderr.write('%s: -S invalid speed %s\n' % + (PROG_NAME, opts['set_speed'])) + sys.exit(1) + elif opt == '-s': + try: + opts['input_speed'] = int(val) + except ValueError: + sys.stderr.write('%s: -s invalid speed %s\n' % + (PROG_NAME, val)) + sys.exit(1) + + if opts['input_speed'] not in gps_model.speeds: + sys.stderr.write('%s: -s invalid speed %s\n' % + (PROG_NAME, opts['input_speed'])) + sys.exit(1) + + elif opt == '-V': + # version + sys.stderr.write('%s: Version %s\n' % (PROG_NAME, gps_version)) + sys.exit(0) + elif opt in '-v': + opts['verbosity'] = int(val) + elif opt == '-w': + try: + opts['input_wait'] = int(val) + except (ValueError): + sys.stderr.write('%s: -w invalid time %s\n' % (PROG_NAME, val)) + sys.exit(1) + + elif opt == '-x': + opts['del_item'].append(val) + elif opt == '-z': + opts['set_item'].append(val) + +if opts['help']: + usage() + +if opts['input_file_name'] is None: + # no input file given + # default to local gpsd + opts['target']['server'] = "localhost" + opts['target']['port'] = gps.GPSD_PORT + opts['target']['device'] = None + if arguments: + # TODO: move to module gps as a function + # server[:port[:device]] + # or maybe ::device + # or maybe \[ipv6\][:port[:device]] + if '[' == arguments[0][0]: + # hex IPv6 address + match = re.match(r'''\[([:a-fA-F0-9]+)\](.*)''', arguments[0]) + opts['target']['server'] = match.group(1) + parts = match.group(2).split(':') + else: + # maybe IPv4 address, maybe hostname + parts = arguments[0].split(':') + if parts[0]: + opts['target']['server'] = parts[0] + + if 1 < len(parts): + if parts[1]: + opts['target']['port'] = parts[1] + if 2 < len(parts) and parts[2]: + opts['target']['device'] = parts[2] + +elif arguments: + sys.stderr.write('%s: Both input file and server specified\n' % PROG_NAME) + sys.exit(1) + +if VERB_PROG <= opts['verbosity']: + # dump versions and all options + print('%s: Version %s\n' % (PROG_NAME, gps_version)) + print('Options:') + for option in sorted(opts): + print(" %s: %s" % (option, opts[option])) + +# done parsing arguments from environment and CLI + +try: + # raw log file requested? + raw = None + if opts['raw_file']: + try: + raw = open(opts['raw_file'], 'w') + except IOError: + sys.stderr.write('%s: failed to open raw file %s\n' % + (PROG_NAME, opts['raw_file'])) + sys.exit(1) + + # create the I/O instance + io_handle = gps_io() + + sys.stdout.flush() + + if opts['disable'] is not None: + if VERB_QUIET < opts['verbosity']: + print('%s: disable %s\n' % (PROG_NAME, opts['disable'])) + if opts['disable'] in gps_model.able_commands: + command = gps_model.able_commands[opts['disable']] + command["command"](gps, 0) + else: + sys.stderr.write('%s: disable %s not found\n' % + (PROG_NAME, opts['disable'])) + sys.exit(1) + + elif opts['enable'] is not None: + if VERB_QUIET < opts['verbosity']: + print('%s: enable %s\n' % (PROG_NAME, opts['enable'])) + if opts['enable'] in gps_model.able_commands: + command = gps_model.able_commands[opts['enable']] + command["command"](gps, 1) + else: + sys.stderr.write('%s: enable %s not found\n' % + (PROG_NAME, opts['enable'])) + sys.exit(1) + + elif opts['poll'] is not None: + if VERB_QUIET < opts['verbosity']: + print('%s: poll %s\n' % (PROG_NAME, opts['poll'])) + + if 'MODEL' == opts["poll"]: + if opts["mode"] is None: + opts["mode"] = 0 # default to portable model + + if opts['poll'] in gps_model.commands: + command = gps_model.commands[opts['poll']] + if (('minVer' in command and + opts['protver'] < command['minVer'])): + print('%s: WARNING poll %s requires protVer >= %s ' + 'you have %s\n' % + (PROG_NAME, opts['poll'], command['minVer'], + opts['protver'])) + + if (('maxVer' in command and + opts['protver'] > command['maxVer'])): + print('%s: WARNING poll %s requires protVer <= %s ' + 'you have %s\n' % + (PROG_NAME, opts['poll'], command['maxVer'], + opts['protver'])) + + if 'opt' in command: + command["command"](gps, command["opt"]) + else: + command["command"](gps) + else: + sys.stderr.write('%s: poll %s not found\n' % + (PROG_NAME, opts['poll'])) + sys.exit(1) + + elif opts['set_speed'] is not None: + gps_model.send_set_speed(opts['set_speed']) + + elif opts['command'] is not None: + cmd_list = opts['command'].split(',') + try: + cmd_data = [int(v, 16) for v in cmd_list] + except ValueError: + badarg = True + else: + data_or = reduce(operator.or_, cmd_data) + badarg = data_or != data_or & 0xFF + if badarg or len(cmd_list) < 2: + sys.stderr.write('%s: Argument format (hex bytes) is' + ' class,id[,payload...]\n' % PROG_NAME) + sys.exit(1) + payload = bytearray(cmd_data[2:]) + if VERB_QUIET < opts['verbosity']: + print('%s: command %s\n' % (PROG_NAME, opts['command'])) + gps_model.gps_send(cmd_data[0], cmd_data[1], payload) + + elif opts['del_item']: + keys = [] + for name in opts['del_item']: + item = gps_model.cfg_by_name(name) + if item: + keys.append(item[1]) + else: + sys.stderr.write('%s: ERROR: item %s unknown\n' % + (PROG_NAME, opts['del_item'])) + exit(1) + gps_model.send_cfg_valdel(keys) + + elif opts['get_item']: + keys = [] + for name in opts['get_item']: + item = gps_model.cfg_by_name(name) + if item: + keys.append(item[1]) + else: + sys.stderr.write('%s: ERROR: item %s unknown\n' % + (PROG_NAME, name)) + exit(1) + gps_model.send_cfg_valget(keys) + + elif opts['set_item']: + nvs = [] + for nv in opts['set_item']: + parts = nv.split(',') + item = gps_model.cfg_by_name(parts[0]) + if item: + nvs.append(nv) + else: + sys.stderr.write('%s: ERROR: item %s unknown\n' % + (PROG_NAME, parts[0])) + exit(1) + gps_model.send_cfg_valset(nvs) + + exit_code = io_handle.read(opts) + + if ((VERB_RAW <= opts['verbosity']) and io_handle.out): + # dump raw left overs + print("Left over data:") + print(io_handle.out) + + sys.stdout.flush() + io_handle.ser.close() + +except KeyboardInterrupt: + print('') + exit_code = 1 + +sys.exit(exit_code) +# vim: set expandtab shiftwidth=4 diff --git a/unit/rtkbase_archive.service b/unit/rtkbase_archive.service new file mode 100644 index 00000000..0f1ef146 --- /dev/null +++ b/unit/rtkbase_archive.service @@ -0,0 +1,7 @@ +[Unit] +Description=RTKBase - Archiving and cleaning raw data + +[Service] +Type=oneshot +User={user} +ExecStart={script_path}/archive_and_clean.sh \ No newline at end of file diff --git a/unit/rtkbase_archive.timer b/unit/rtkbase_archive.timer new file mode 100644 index 00000000..ea3a5130 --- /dev/null +++ b/unit/rtkbase_archive.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Run rtkbase_archive.service everyday at 04H00 + +[Timer] +OnCalendar=*-*-* 04:00:00 +Persistent=true + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/unit/rtkbase_web.service b/unit/rtkbase_web.service new file mode 100644 index 00000000..b061d95f --- /dev/null +++ b/unit/rtkbase_web.service @@ -0,0 +1,13 @@ +[Unit] +Description=RTKBase Web Server +#After=network-online.target +#Wants=network-online.target + +[Service] +User=root +ExecStart=python3 {script_path}/web_app/server.py +Restart=on-failure +RestartSec=30 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/unit/str2str_file.service b/unit/str2str_file.service index 1f2dd0d1..a5e17ecb 100644 --- a/unit/str2str_file.service +++ b/unit/str2str_file.service @@ -1,6 +1,6 @@ [Unit] Description=RTKBase File - Log data -Requires=str2str_tcp.service systemd-timesyncd.service +Requires=str2str_tcp.service [Service] Type=forking @@ -8,7 +8,9 @@ User={user} ExecStart={script_path}/run_cast.sh in_tcp out_file Restart=on-failure RestartSec=30 - +#Limiting log to 1 msg per minute +LogRateLimitIntervalSec=1 minute +LogRateLimitBurst=1 [Install] WantedBy=multi-user.target diff --git a/unit/str2str_ntrip.service b/unit/str2str_ntrip.service index 4d6ea7f4..1da35a82 100644 --- a/unit/str2str_ntrip.service +++ b/unit/str2str_ntrip.service @@ -6,10 +6,13 @@ Requires=str2str_tcp.service [Service] Type=forking -#User=basegnss +User={user} ExecStart={script_path}/run_cast.sh in_tcp out_caster Restart=on-failure RestartSec=30 +#Limiting log to 1 msg per minute +LogRateLimitIntervalSec=1 minute +LogRateLimitBurst=1 [Install] WantedBy=multi-user.target diff --git a/unit/str2str_rtcm_svr.service b/unit/str2str_rtcm_svr.service new file mode 100644 index 00000000..452b7f1e --- /dev/null +++ b/unit/str2str_rtcm_svr.service @@ -0,0 +1,18 @@ +[Unit] +Description=RTKBase rtcm server +#After=network-online.target +#Wants=network-online.target +Requires=str2str_tcp.service + +[Service] +Type=forking +User={user} +ExecStart={script_path}/run_cast.sh in_tcp out_rtcm_svr +Restart=on-failure +RestartSec=30 +#Limiting log to 1 msg per minute +LogRateLimitIntervalSec=1 minute +LogRateLimitBurst=1 + +[Install] +WantedBy=multi-user.target diff --git a/unit/str2str_tcp.service b/unit/str2str_tcp.service index 9a5dc943..91c6b9db 100644 --- a/unit/str2str_tcp.service +++ b/unit/str2str_tcp.service @@ -2,15 +2,17 @@ Description=RTKBase Tcp #After=network-online.target #Wants=network-online.target -Requires=network-online.target +#Requires=network-online.target [Service] Type=forking -#User=basegnss +User={user} ExecStart={script_path}/run_cast.sh in_serial out_tcp Restart=on-failure RestartSec=30 - +#Limiting log to 1 msg per minute +LogRateLimitIntervalSec=1 minute +LogRateLimitBurst=1 [Install] WantedBy=multi-user.target diff --git a/web_app/ConfigManager.py b/web_app/ConfigManager.py new file mode 100644 index 00000000..290df07f --- /dev/null +++ b/web_app/ConfigManager.py @@ -0,0 +1,368 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +from reach_tools import reach_tools + +import os +from glob import glob +from shutil import copy, Error + +# This module aims to make working with RTKLIB configs easier +# It allows to parse RTKLIB .conf files to python dictionaries and backwards +# Note that on startup it reads on of the default configs +# and keeps the order of settings, stored there + +class Config: + + def __init__(self, file_name = None, items = None): + # we keep all the options for current config file and their order here + + # config file name + self.current_file_name = None + + # due to how sending stuff over socket.io works, we need to keep config data + # as a dictionary. to maintain order later in the browser we keep it in the + # following form: + # items = { + # "0": item0, + # "1": item1 + # } + # where item is also a dictionary: + # item0 = { + # "parameter": "p", + # "value": "v", + # "comment": "c", + # "description": "d" + # } + + if items is None: + self.items = {} + else: + self.items = items + + # if we pass the file to the constructor, then read the values + if file_name is not None: + self.readFromFile(file_name) + + def formStringFromItem(self, item): + # form a line to put into a RTKLIB config file: + + # item must be a dict, described in the __init__ method comments! + + # we want to write values aligned for easier reading + # hence need to add a number of spaces after the parameter + if item: + parameter_with_trailing_spaces = item["parameter"] + " " * (18 - len(item["parameter"])) + + s = [parameter_with_trailing_spaces, "=" + item["value"]] + + if "comment" in item: + s.append("#") + s.append(item["comment"]) + + if "description" in item: + s.append("##") + s.append(item["description"]) + + return " ".join(s) + else: + return "" + + def extractItemFromString(self, string): + # extract information from a config file line + # return true, if the line + + # create an empty item + item = {} + + # cut the line into pieces by spaces + separated_lines = string.split() + length = len(separated_lines) + + # first, check if this line is empty + if length > 0: + + # second, check if it's fully commented + if separated_lines[0][0] != "#": + + # extract the parameter and value + item["parameter"] = separated_lines[0] + item["value"] = separated_lines[1][1:] + + # check if we have more info, possibly useful comment + if length > 3 and separated_lines[2] == "#": + item["comment"] = separated_lines[3] + + # check if we have more info, possibly description + if length > 5 and separated_lines[4] == "##": + # in order to have a description with spaces, we take all what's left + # after the "##" symbols and create a single line out of it: + description = separated_lines[5:] + description = " ".join(description) + item["description"] = description + + # check if we have only a description, rather than a comment and description + if length >3 and separated_lines[2] == "##": + description = separated_lines[3:] + description = " ".join(description) + item["description"] = description + + # add information about available serial connections to input and output paths + if "path" in item["parameter"]: + item["comment"] = self.formSelectCommentFromList(reach_tools.getAvailableSerialPorts()) + + # we return the item we managed to extract form from string. if it's empty, + # then we could not parse the string, hence it's empty, commented, or invalid + return item + + def formSelectCommentFromList(self, items_list): + comment = "" + + if items_list: + comment = "(" + + for index, item in enumerate(items_list): + comment += str(index) + ":" + str(item) + "," + + comment = comment[:-1] + ")" + + return comment + + def parseBluetoothEntries(self, config_dict): + # check if anything is set as a tcpsvr with path :8143 + # and change it to bluetooth + entries_with_bt_port = {k: v for (k, v) in config_dict.items() if v["value"] == "localhost:8143"} + + for entry in entries_with_bt_port.keys(): + # can be log or out or in + io_field = entries_with_bt_port[entry]["parameter"].split("-")[0] + + # find the corresponding io type + io_type_entries = {k: v for (k, v) in config_dict.items() if io_field + "-type" in v["parameter"]} + + for key in io_type_entries.keys(): + config_dict[key]["value"] = "bluetooth" + + return config_dict + + def processConfig(self, config_dict): + # sometimes, when reading we need to handle special situations, like bluetooth connection + + config_dict = self.parseBluetoothEntries(config_dict) + + return config_dict + + def readFromFile(self, from_file): + + # save file name as current + self.current_file_name = from_file + + # clear previous data + self.items = {} + items_dict = {} + + # current item container + item = {} + + with open(from_file, "r") as f: + i = 0 + for line in f: + # we mine for info in every line of the file + # if the info is valid, we add this item to the items dict + item = self.extractItemFromString(line) + + if item: + # save the info as {"0": item0, ...} + items_dict[str(i)] = item + + i += 1 + + self.items = self.processConfig(items_dict) + + def writeToFile(self, to_file = None): + + # by default, we write to the current file + if to_file == None: + to_file = self.current_file_name + + # we keep the config as a dict, which is unordered + # now is a time to convert it to a list, so that we could + # write it to a file maintaining the order + + # create an empty list of the same length as we have items + items_list = [""] * len(self.items) + + # turn our dict with current items into a list in the correct order: + for item_number in self.items: + # some of the fields are not numbers and need to be treated separately + try: + int_item_number = int(item_number) + except ValueError: + pass + else: + items_list[int_item_number] = self.items[item_number] + + with open(to_file, "w") as f: + line = "# rtkrcv options for rtk (v.2.4.2)" + f.write(line + "\n\n") + + for item in items_list: + f.write(self.formStringFromItem(item) + "\n") + +class ConfigManager: + + def __init__(self, rtklib_path, config_path): + + self.config_path = config_path + + self.default_rover_config = "rtkbase_single_default.conf" + self.default_base_config = "rtkbase_base_default.conf" + + self.available_configs = [] + self.updateAvailableConfigs() + + # create a buffer for keeping config data + # read default one into buffer + + self.buffered_config = Config(os.path.join(self.config_path, self.default_rover_config)) + + def updateAvailableConfigs(self): + + self.available_configs = [] + + # get a list of available .conf files in the config directory + configs = glob(self.config_path + "*.conf") + self.available_configs = [os.path.basename(config) for config in configs] + + + # we do not show the base config + try: + self.available_configs.remove(self.default_base_config) + except: + pass + + def readConfig(self, from_file): + + if from_file is None: + from_file = self.default_rover_config + + # check if this is a full path or just a name + # if it's a name, then we use the default location + if "/" in from_file: + config_file_path = from_file + else: + config_file_path = self.config_path + from_file + + self.buffered_config.readFromFile(config_file_path) + + def writeConfig(self, to_file = None, config_values = None): + + if to_file is None: + to_file = self.default_rover_config + + # check if this is a full path or just a name + # if it's a name, then we use the default location + if "/" not in to_file: + to_file = self.config_path + to_file + + # do the actual writing + + # if we receive config_values to write, then we create another config instance + # and use write on it + if config_values is None: + self.buffered_config.writeToFile(to_file) + else: + conf = Config(items = config_values) + conf.writeToFile(to_file) + + def resetConfigToDefault(self, config_name): + # try to copy default config to the working configs directory + if "/" not in config_name: + default_config_value = self.config_path + config_name + else: + default_config_value = config_name + + try: + copy(default_config_value, self.config_path) + except IOError as e: + print("Error resetting config " + config_name + " to default. Error: " + e.filename + " - " + e.strerror) + except OSError as e: + print('Error: %s' % e) + + def deleteConfig(self, config_name): + # try to delete config if it exists + if "/" not in config_name: + config_name = self.config_path + config_name + + try: + os.remove(config_name) + except OSError as e: + print ("Error: " + e.filename + " - " + e.strerror) + + def readItemFromConfig(self, property, from_file): + # read a complete item from config, found by "parameter part" + + conf = Config(self.config_path + from_file) + + # cycle through this config to find the needed option + for item_number in conf.items: + if conf.items[item_number]["parameter"] == property: + # in case we found it + return conf.items[item_number] + + # in case we didn't + return None + + def writeItemToConfig(self, item, to_file): + # write a complete item to the file + + # first we read the whole file + conf = Config(self.config_path + to_file) + + # then we substitute the one property + # cycle through this config to find the needed option + for item_number in conf.items: + if conf.items[item_number]["parameter"] == item["parameter"]: + # in case we found it + conf.items[item_number] = item + + # rewrite the file again: + self.writeConfig(to_file, conf.items) + return 1 + break + + # in case we didn't find it + return None + + + + + + + + + + + + + diff --git a/web_app/LICENSE.txt b/web_app/LICENSE.txt new file mode 100644 index 00000000..20d40b6b --- /dev/null +++ b/web_app/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/web_app/LogManager.py b/web_app/LogManager.py new file mode 100644 index 00000000..4597ff88 --- /dev/null +++ b/web_app/LogManager.py @@ -0,0 +1,179 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import os +import math +from glob import glob + +from log_converter import convbin + +class LogManager(): + + supported_solution_formats = ["llh", "xyz", "enu", "nmea", "erb", "zip", "bz2", "tar", "tag"] + + def __init__(self, rtklib_path, log_path): + + self.log_path = log_path + self.convbin = convbin.Convbin(rtklib_path) + + self.log_being_converted = "" + + self.available_logs = [] + self.updateAvailableLogs() + + def updateAvailableLogs(self): + + self.available_logs = [] + + print("Getting a list of available logs") + for log in glob(self.log_path + "/*"): + + log_name = os.path.basename(log) + # get size in bytes and convert to MB + log_size = self.getLogSize(log) + + potential_zip_path = os.path.splitext(log)[0] + ".zip" + + log_format = self.getLogFormat(log) + is_being_converted = True if log == self.log_being_converted else False + + self.available_logs.append({ + "name": log_name, + "size": log_size, + "format": log_format, + "is_being_converted": is_being_converted + }) + + self.available_logs.sort(key = lambda date: date['name'], reverse = True) + + #Adding an id to each log + id = 0 + for log in self.available_logs: + log['id'] = id + id += 1 + + + """ + def getLogCompareString(self, log_name): + name_without_extension = os.path.splitext(log_name)[0] + print("log name: ", log_name) + log_type, log_date = name_without_extension.split("_") + return log_date + log_type[0:2] + """ + def getLogSize(self, log_path): + size = os.path.getsize(log_path) / (1024 * 1024.0) + return "{0:.2f}".format(size) + + def getLogFormat(self, log_path): + file_path, extension = os.path.splitext(log_path) + extension = extension[1:] + + """ + # removed because a zip file is not necesseraly RINEX + potential_zip_path = file_path + ".zip" + if os.path.isfile(potential_zip_path): + return "RINEX" + """ + if (extension in self.supported_solution_formats or + extension in self.convbin.supported_log_formats): + return extension.upper() + else: + return "" + + def formTimeString(self, seconds): + # form a x minutes y seconds string from seconds + m, s = divmod(seconds, 60) + + s = math.ceil(s) + + format_string = "{0:.0f} minutes " if m > 0 else "" + format_string += "{1:.0f} seconds" + + return format_string.format(m, s) + + def calculateConversionTime(self, log_path): + # calculate time to convert based on log size and format + log_size = os.path.getsize(log_path) / (1024*1024.0) + conversion_time = 0 + + if log_path.endswith("rtcm3"): + conversion_time = 42.0 * log_size + elif log_path.endswith("ubx"): + conversion_time = 1.8 * log_size + + return "{:.0f}".format(conversion_time) + + def cleanLogFiles(self, log_path): + # delete all files except for the raw log + full_path_logs = glob(self.log_path + "/*.rtcm3") + glob(self.log_path + "/*.ubx") + extensions_not_to_delete = [".zip", ".ubx", ".rtcm3"] + + log_without_extension = os.path.splitext(log_path)[0] + log_files = glob(log_without_extension + "*") + + for log_file in log_files: + if not any(log_file.endswith(ext) for ext in extensions_not_to_delete): + try: + os.remove(log_file) + except OSError as e: + print ("Error: " + e.filename + " - " + e.strerror) + + def deleteLog(self, log_filename): + # try to delete log if it exists + + #log_name, extension = os.path.splitext(log_filename) + + # try to delete raw log + print("Deleting log " + log_filename) + try: + os.remove(os.path.join(self.log_path, log_filename)) + except OSError as e: + print ("Error: " + e.log_filename + " - " + e.strerror) + + """ + print("Deleting log " + log_name + ".zip") + try: + os.remove(self.log_path + "/" + log_name + ".zip") + except OSError as e: + print ("Error: " + e.filename + " - " + e.strerror) + """ + + def getRINEXVersion(self): + # read RINEX version from system file + print("Getting RINEX version from system settings") + version = "3.01" + try: + with open(os.path.join(os.path.expanduser("~"), ".reach/rinex_version"), "r") as f: + version = f.readline().rstrip("\n") + except (IOError, OSError): + print("No such file detected, defaulting to 3.01") + + return version + + def setRINEXVersion(self, version): + # write RINEX version to system file + print("Writing new RINEX version to system file") + + with open(os.path.join(os.path.expanduser("~"), ".reach/rinex_version"), "w") as f: + f.write(version) + diff --git a/web_app/RTKBaseConfigManager.py b/web_app/RTKBaseConfigManager.py new file mode 100644 index 00000000..bc713f4c --- /dev/null +++ b/web_app/RTKBaseConfigManager.py @@ -0,0 +1,179 @@ +import os +from configparser import ConfigParser +from secrets import token_urlsafe + +class RTKBaseConfigManager: + """ A class to easily access the settings from RTKBase settings.conf """ + + NON_QUOTED_KEYS = ("basedir", "web_authentification", "new_web_password", "web_password_hash", + "flask_secret_key", "archive_name", "user") + + def __init__(self, default_settings_path, user_settings_path): + """ + :param default_settings_path: path to the default settings file + :param user_settings_path: path to the user settings file + """ + self.user_settings_path = user_settings_path + self.config = self.merge_default_and_user(default_settings_path, user_settings_path) + self.expand_path() + self.write_file(self.config) + + def merge_default_and_user(self, default, user): + """ + After a software update if there is some new entries in the default settings file, + we need to add them to the user settings file. This function will do this: It loads + the default settings then overwrite the existing values from the user settings. Then + the function write these settings on disk. + + :param default: path to the default settings file + :param user: path to the user settings file + :return: the new config object + """ + config = ConfigParser(interpolation=None) + config.read(default) + #if there is no existing user settings file, config.read return + #an empty object. + config.read(user) + return config + + + def parseconfig(self, settings_path): + """ + Parse the config file with interpolation=None or it will break run_cast.sh + """ + config = ConfigParser(interpolation=None) + config.read(settings_path) + return config + + def expand_path(self): + """ + get the paths and convert $BASEDIR to the real path + """ + datadir = self.config.get("local_storage", "datadir") + if "$BASEDIR" in datadir: + exp_datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../", datadir.strip("$BASEDIR/"))) + self.update_setting("local_storage", "datadir", exp_datadir) + + logdir = self.config.get("log", "logdir") + if "$BASEDIR" in logdir: + exp_logdir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../", logdir.strip("$BASEDIR/"))) + self.update_setting("log", "logdir", exp_logdir) + + + + def listvalues(self): + """ + print all keys/values from all sections in the settings + """ + for section in self.config.sections(): + print("SECTION: {}".format(section)) + for key in self.config[section]: + print("{} : {} ".format(key, self.config[section].get(key))) + + def get_main_settings(self): + """ + Get a subset of the settings from the main section in an ordered object + and remove the single quotes. + """ + ordered_main = [{"source_section" : "main"}] + for key in ("position", "com_port", "com_port_settings", "receiver", "receiver_format", "tcp_port"): + ordered_main.append({key : self.config.get('main', key).strip("'")}) + return ordered_main + + def get_ntrip_settings(self): + """ + Get a subset of the settings from the ntrip section in an ordered object + and remove the single quotes. + """ + ordered_ntrip = [{"source_section" : "ntrip"}] + for key in ("svr_addr", "svr_port", "svr_pwd", "mnt_name", "rtcm_msg"): + ordered_ntrip.append({key : self.config.get('ntrip', key).strip("'")}) + return ordered_ntrip + + def get_file_settings(self): + """ + Get a subset of the settings from the file section in an ordered object + and remove the single quotes. + """ + ordered_file = [{"source_section" : "local_storage"}] + for key in ("datadir", "file_name", "file_rotate_time", "file_overlap_time", "archive_rotate"): + ordered_file.append({key : self.config.get('local_storage', key).strip("'")}) + return ordered_file + + def get_rtcm_svr_settings(self): + """ + Get a subset of the settings from the file section in an ordered object + and remove the single quotes. + """ + ordered_rtcm_svr = [{"source_section" : "rtcm_svr"}] + for key in ("rtcm_svr_port", "rtcm_svr_msg"): + ordered_rtcm_svr.append({key : self.config.get('rtcm_svr', key).strip("'")}) + return ordered_rtcm_svr + + def get_ordered_settings(self): + """ + Get a subset of the main, ntrip and file sections from the settings file + Return a dict where values are a list (to keeps the settings ordered) + """ + ordered_settings = {} + ordered_settings['main'] = self.get_main_settings() + ordered_settings['ntrip'] = self.get_ntrip_settings() + ordered_settings['file'] = self.get_file_settings() + ordered_settings['rtcm_svr'] = self.get_rtcm_svr_settings() + return ordered_settings + + def get_web_authentification(self): + """ + a simple method to convert the web_authentification value + to a boolean + :return boolean + """ + return self.config.getboolean("general", "web_authentification") + + def get_secret_key(self): + """ + Return the flask secret key, or generate a new one if it doesn't exists + """ + SECRET_KEY = self.config.get("general", "flask_secret_key", fallback='None') + if SECRET_KEY is 'None' or SECRET_KEY == '': + SECRET_KEY = token_urlsafe(48) + self.update_setting("general", "flask_secret_key", SECRET_KEY) + + return SECRET_KEY + + def get(self, *args, **kwargs): + """ + a wrapper around configparser.get() + """ + return self.config.get(*args, **kwargs) + + def update_setting(self, section, setting, value, write_file=True): + """ + Update a setting in the config file and write the file (default) + If the setting is not in the NON_QUOTED_KEYS list, the method will + add single quotes + :param section: the section in the config file + :param setting: the setting (like a key in a dict) + :param value: the new value for the setting + :param write_file: write the file or not + """ + #Add single quotes around the value + if setting not in self.NON_QUOTED_KEYS: + value = "'" + value + "'" + try: + self.config[section][setting] = value + if write_file: + self.write_file() + except Exception as e: + print(e) + return False + + def write_file(self, settings=None): + """ + write on disk the settings to the config file + """ + if settings is None: + settings = self.config + + with open(self.user_settings_path, "w") as configfile: + settings.write(configfile, space_around_delimiters=False) diff --git a/web_app/RTKLIB.py b/web_app/RTKLIB.py new file mode 100644 index 00000000..a37e7d1b --- /dev/null +++ b/web_app/RTKLIB.py @@ -0,0 +1,704 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +from RtkController import RtkController +from ConfigManager import ConfigManager +from Str2StrController import Str2StrController +from LogManager import LogManager +#from ReachLED import ReachLED +from reach_tools import reach_tools, gps_time + +import json +import time +import os +import signal +import zipfile + +from subprocess import check_output, Popen, PIPE +from threading import Semaphore, Thread + +# master class for working with all RTKLIB programmes +# prevents them from stacking up and handles errors +# also handles all data broadcast through websockets + +class RTKLIB: + + # we will save RTKLIB state here for later loading + state_file = os.path.join(os.path.expanduser("~"), ".reach/rtk_state") + # if the state file is not available, these settings are loaded + default_state = { + "started": "no", + "state": "base" + } + + def __init__(self, socketio, rtklib_path = None, config_path=None, enable_led = True, log_path = None): + + print("RTKLIB 1") + print(rtklib_path) + print(log_path) + + if rtklib_path is None: + rtklib_path = os.path.join(os.path.expanduser("~"), "RTKLIB") + + if config_path is None: + self.config_path = os.path.join(os.path.dirname(__file__), "rtklib_configs") + else: + self.config_path = config_path + + if log_path is None: + #TODO find a better default location + self.log_path = "../data" + else: + self.log_path = log_path + + # This value should stay below the timeout value or the Satellite/Coordinate broadcast + # thread will stop + self.sleep_count = 0 + + # default state for RTKLIB is "rover single" + self.state = "base" + + # we need this to broadcast stuff + self.socketio = socketio + + # these are necessary to handle rover mode + self.rtkc = RtkController(rtklib_path, self.config_path) + self.conm = ConfigManager(rtklib_path, self.config_path) + + # this one handles base settings + self.s2sc = Str2StrController(rtklib_path) + + # take care of serving logs + self.logm = LogManager(rtklib_path, self.log_path) + + # basic synchronisation to prevent errors + self.semaphore = Semaphore() + + # we need this to send led signals +# self.enable_led = enable_led + +# if self.enable_led: +# self.led = ReachLED() + + # broadcast satellite levels and status with these + self.server_not_interrupted = True + self.satellite_thread = None + self.coordinate_thread = None + self.conversion_thread = None + + self.system_time_correct = False +# self.system_time_correct = True + + self.time_thread = Thread(target = self.setCorrectTime) + self.time_thread.start() + + # we try to restore previous state + # in case we can't, we start as rover in single mode + # self.loadState() + + def setCorrectTime(self): + # determine if we have ntp service ready or we need gps time + + print("RTKLIB 2 GPS time sync") + +## if not gps_time.time_synchronised_by_ntp(): + # wait for gps time +## print("Time is not synced by NTP") +# self.updateLED("orange,off") +# gps_time.set_gps_time("/dev/ttyACM0", 115200) + + print("Time is synced by GPS!") + + self.system_time_correct = True + self.socketio.emit("system time corrected", {}, namespace="/test") + + self.loadState() + self.socketio.emit("system state reloaded", {}, namespace="/test") + + + + def launchBase(self): + # due to the way str2str works, we can't really separate launch and start + # all the configuration goes to startBase() function + # this launchBase() function exists to change the state of RTKLIB instance + # and to make the process for base and rover modes similar + + self.semaphore.acquire() + + self.state = "base" + + #self.saveState() + +# if self.enable_led: +# self.updateLED() + + print("RTKLIB 7 Base mode launched") + + + self.semaphore.release() + + def shutdownBase(self): + # due to the way str2str works, we can't really separate launch and start + # all the configuration goes to startBase() function + # this shutdownBase() function exists to change the state of RTKLIB instance + # and to make the process for base and rover modes similar + + self.stopBase() + + self.semaphore.acquire() + + self.state = "inactive" + + + print("RTKLIB 8 Base mode shutdown") + + self.semaphore.release() + + def startBase(self, rtcm3_messages = None, base_position = None, gps_cmd_file = None): + + self.semaphore.acquire() + """ + print("RTKLIB 9 Attempting to start str2str...") + + + res = self.s2sc.start(rtcm3_messages, base_position, gps_cmd_file) + if res < 0: + print("str2str start failed") + elif res == 1: + print("str2str start successful") + elif res == 2: + print("str2str already started") + + self.saveState() + """ + #TODO need refactoring + #maybe a new method to launch/start rtkrcv outside + #startBase and startRover + #TODO launchRover and startRover send a config_name to rtkc + #I don't do this here :-/ + print("RTKLIB 9a Attempting to launch rtkrcv...") + + res2 = self.rtkc.launch() + + if res2 < 0: + print("rtkrcv launch failed") + elif res2 == 1: + print("rtkrcv launch successful") + elif res2 == 2: + print("rtkrcv already launched") + + #TODO need refactoring + #maybe a new method to launch/start rtkrcv outside + #startBase and startRover + print("RTKLIB 9b Attempting to start rtkrcv...") + res3 = self.rtkc.start() + + if res3 == -1: + print("rtkrcv start failed") + elif res3 == 1: + print("rtkrcv start successful") + print("Starting coordinate and satellite broadcast") + elif res3 == 2: + print("rtkrcv already started") + + # start fresh data broadcast + #TODO the satellite and coordinate broadcast start + #when rtkrcv start failed + + self.server_not_interrupted = True + + if self.satellite_thread is None: + self.satellite_thread = Thread(target = self.broadcastSatellites) + self.satellite_thread.start() + + if self.coordinate_thread is None: + self.coordinate_thread = Thread(target = self.broadcastCoordinates) + self.coordinate_thread.start() + + self.semaphore.release() + + return res3 + + def stopBase(self): + + self.semaphore.acquire() + + + print("RTKLIB 10a Attempting to stop rtkrcv...") + + res2 = self.rtkc.stop() + if res2 == -1: + print("rtkrcv stop failed") + elif res2 == 1: + print("rtkrcv stop successful") + elif res2 == 2: + print("rtkrcv already stopped") + + print("RTKLIB 10b Attempting to stop satellite broadcasting...") + + self.server_not_interrupted = False + + if self.satellite_thread is not None: + self.satellite_thread.join() + self.satellite_thread = None + + if self.coordinate_thread is not None: + self.coordinate_thread.join() + self.coordinate_thread = None + + print("RTKLIB 10c Attempting rtkrcv shutdown") + + res = self.rtkc.shutdown() + + if res < 0: + print("rtkrcv shutdown failed") + elif res == 1: + print("rtkrcv shutdown successful") + self.state = "inactive" + elif res == 2: + print("rtkrcv already shutdown") + self.state = "inactive" + self.semaphore.release() + + return res + + def readConfigBase(self): + + self.semaphore.acquire() + + print("RTKLIB 11 Got signal to read base config") + + self.socketio.emit("current config base", self.s2sc.readConfig(), namespace = "/test") + + self.semaphore.release() + + def writeConfigBase(self, config): + + self.semaphore.acquire() + + print("RTKLIB 12 Got signal to write base config") + + self.s2sc.writeConfig(config) + + print("Restarting str2str...") + + res = self.s2sc.stop() + self.s2sc.start() + + if res > 1: + print("Restart successful") + else: + print("Restart failed") + + self.saveState() + +# if self.enable_led: +# self.updateLED() + + self.semaphore.release() + + return res + + def shutdown(self): + # shutdown whatever mode we are in. stop broadcast threads + + print("RTKLIB 17 Shutting down") + + # clean up broadcast and blink threads + self.server_not_interrupted = False +# self.led.blinker_not_interrupted = False + + if self.coordinate_thread is not None: + self.coordinate_thread.join() + + if self.satellite_thread is not None: + self.satellite_thread.join() + +# if self.led.blinker_thread is not None: +# self.led.blinker_thread.join() + + # shutdown base + + elif self.state == "base": + return self.shutdownBase() + + # otherwise, we are inactive + return 1 + + def deleteConfig(self, config_name): + # pass deleteConfig to conm + + print("RTKLIB 18 Got signal to delete config " + config_name) + + self.conm.deleteConfig(config_name) + + self.conm.updateAvailableConfigs() + + # send available configs to the browser + self.socketio.emit("available configs", {"available_configs": self.conm.available_configs}, namespace="/test") + + print(self.conm.available_configs) + + def cancelLogConversion(self, raw_log_path): + if self.logm.log_being_converted: + print("Canceling log conversion for " + raw_log_path) + + self.logm.convbin.child.kill(signal.SIGINT) + + self.conversion_thread.join() + self.logm.convbin.child.close(force = True) + + print("Thread killed") + self.logm.cleanLogFiles(raw_log_path) + self.logm.log_being_converted = "" + + print("Canceled msg sent") + + def processLogPackage(self, raw_log_path): + + currently_converting = False + + try: + print("conversion thread is alive " + str(self.conversion_thread.isAlive())) + currently_converting = self.conversion_thread.isAlive() + except AttributeError: + pass + + log_filename = os.path.basename(raw_log_path) + potential_zip_path = os.path.splitext(raw_log_path)[0] + ".zip" + + can_send_file = True + + # can't send if there is no converted package and we are busy + if (not os.path.isfile(potential_zip_path)) and (currently_converting): + can_send_file = False + + if can_send_file: + print("Starting a new bg conversion thread for log " + raw_log_path) + self.logm.log_being_converted = raw_log_path + self.conversion_thread = Thread(target = self.getRINEXPackage, args = (raw_log_path, )) + self.conversion_thread.start() + else: + error_msg = { + "name": os.path.basename(raw_log_path), + "conversion_status": "A log is being converted at the moment. Please wait", + "messages_parsed": "" + } + self.socketio.emit("log conversion failed", error_msg, namespace="/test") + + def conversionIsRequired(self, raw_log_path): + log_filename = os.path.basename(raw_log_path) + potential_zip_path = os.path.splitext(raw_log_path)[0] + ".zip" + + print("Comparing " + raw_log_path + " and " + potential_zip_path + " for conversion") + + if os.path.isfile(potential_zip_path): + print("Raw logs differ " + str(self.rawLogsDiffer(raw_log_path, potential_zip_path))) + return self.rawLogsDiffer(raw_log_path, potential_zip_path) + else: + print("No zip file!!! Conversion required") + return True + + def rawLogsDiffer(self, raw_log_path, zip_package_path): + # check if the raw log is the same size in the zip and in filesystem + log_name = os.path.basename(raw_log_path) + raw_log_size = os.path.getsize(raw_log_path) + + zip_package = zipfile.ZipFile(zip_package_path) + raw_file_inside_info = zip_package.getinfo("Raw/" + log_name) + raw_file_inside_size = raw_file_inside_info.file_size + + print("Sizes:") + print("Inside: " + str(raw_file_inside_size)) + print("Raw: " + str(raw_log_size)) + + if raw_log_size == raw_file_inside_size: + return False + else: + return True + + def getRINEXPackage(self, raw_log_path): + # if this is a solution log, return the file right away + if "sol" in raw_log_path: + log_url_tail = "/logs/download/" + os.path.basename(raw_log_path) + self.socketio.emit("log download path", {"log_url_tail": log_url_tail}, namespace="/test") + return raw_log_path + + # return RINEX package if it already exists + # create one if not + log_filename = os.path.basename(raw_log_path) + potential_zip_path = os.path.splitext(raw_log_path)[0] + ".zip" + result_path = "" + + if self.conversionIsRequired(raw_log_path): + print("Conversion is Required!") + result_path = self.createRINEXPackage(raw_log_path) + # handle canceled conversion + if result_path is None: + log_url_tail = "/logs/download/" + os.path.basename(raw_log_path) + self.socketio.emit("log download path", {"log_url_tail": log_url_tail}, namespace="/test") + return None + else: + result_path = potential_zip_path + print("Conversion is not Required!") + already_converted_package = { + "name": log_filename, + "conversion_status": "Log already converted. Details inside the package", + "messages_parsed": "" + } + self.socketio.emit("log conversion results", already_converted_package, namespace="/test") + + log_url_tail = "/logs/download/" + os.path.basename(result_path) + self.socketio.emit("log download path", {"log_url_tail": log_url_tail}, namespace="/test") + + self.cleanBusyMessages() + self.logm.log_being_converted = "" + + return result_path + + def cleanBusyMessages(self): + # if user tried to convert other logs during conversion, he got an error message + # this function clears them to show it's ok to convert again + self.socketio.emit("clean busy messages", {}, namespace="/test") + + def createRINEXPackage(self, raw_log_path): + # create a RINEX package before download + # in case we fail to convert, return the raw log path back + result = raw_log_path + log_filename = os.path.basename(raw_log_path) + + conversion_time_string = self.logm.calculateConversionTime(raw_log_path) + + start_package = { + "name": log_filename, + "conversion_time": conversion_time_string + } + + conversion_result_package = { + "name": log_filename, + "conversion_status": "", + "messages_parsed": "", + "log_url_tail": "" + } + + self.socketio.emit("log conversion start", start_package, namespace="/test") + try: + log = self.logm.convbin.convertRTKLIBLogToRINEX(raw_log_path, self.logm.getRINEXVersion()) + except (ValueError, IndexError): + print("Conversion canceled") + conversion_result_package["conversion_status"] = "Conversion canceled, downloading raw log" + self.socketio.emit("log conversion results", conversion_result_package, namespace="/test") + return None + + print("Log conversion done!") + + if log is not None: + result = log.createLogPackage() + if log.isValid(): + conversion_result_package["conversion_status"] = "Log converted to RINEX" + conversion_result_package["messages_parsed"] = log.log_metadata.formValidMessagesString() + else: + conversion_result_package["conversion_status"] = "Conversion successful, but log does not contain any useful data. Downloading raw log" + else: + print("Could not convert log. Is the extension wrong?") + conversion_result_package["conversion_status"] = "Log conversion failed. Downloading raw log" + + self.socketio.emit("log conversion results", conversion_result_package, namespace="/test") + + print("Log conversion results:") + print(str(log)) + + return result + + def saveState(self): + # save current state for future resurrection: + # state is a list of parameters: + # rover state example: ["rover", "started", "reach_single_default.conf"] + # base state example: ["base", "stopped", "input_stream", "output_stream"] + + state = {} + + # save "rover", "base" or "inactive" state + state["state"] = self.state + + if self.state == "rover": + started = self.rtkc.started + elif self.state == "base": + started = self.s2sc.started + elif self.state == "inactive": + started = False + + state["started"] = "yes" if started else "no" + + # dump rover state + state["rover"] = {"current_config": self.rtkc.current_config} + + # dump rover state + state["base"] = { + "input_stream": self.s2sc.input_stream, + "output_stream": self.s2sc.output_stream, + "rtcm3_messages": self.s2sc.rtcm3_messages, + "base_position": self.s2sc.base_position, + "gps_cmd_file": self.s2sc.gps_cmd_file + } + + print("RTKLIB 20 DEBUG saving state") + print(str(state)) + + with open(self.state_file, "w") as f: + json.dump(state, f, sort_keys = True, indent = 4) + + reach_tools.run_command_safely(["sync"]) + + return state + + def byteify(self, input): + # thanks to Mark Amery from StackOverflow for this awesome function + if isinstance(input, dict): + return {self.byteify(key): self.byteify(value) for key, value in input.items()} + elif isinstance(input, list): + return [self.byteify(element) for element in input] + elif isinstance(input, str): + #no need to convert to utf-8 anymore with Python v3.x + #return input.encode('utf-8') + return input + else: + return input + + def getState(self): + # get the state, currently saved in the state file + print("RTKLIB 21 Trying to read previously saved state...") + + try: + f = open(self.state_file, "r") + except IOError: + # can't find the file, let's create a new one with default state + print("Could not find existing state, Launching default mode...") + + return self.default_state + else: + + print("Found existing state...trying to decode...") + + try: + json_state = json.load(f) + except ValueError: + # could not properly decode current state + print("Could not decode json state. Launching default mode...") + f.close() + + return self.default_state + else: + print("Decoding succesful") + + f.close() + + # convert unicode strings to normal + json_state = self.byteify(json_state) + + print("That's what we found:") + print(str(json_state)) + + return json_state + + def loadState(self): + + # get current state + json_state = self.getState() + + print("RTKLIB 22 Now loading the state printed above... ") + #print(str(json_state)) + # first, we restore the base state, because no matter what we end up doing, + # we need to restore base state + + if json_state["state"] == "base": + self.launchBase() + + if json_state["started"] == "yes": + self.startBase() + + print(str(json_state["state"]) + " state loaded") + + def sendState(self): + # send current state to every connecting browser + + state = self.getState() + print("RTKLIB 22a") + #print(str(state)) + self.conm.updateAvailableConfigs() + state["available_configs"] = self.conm.available_configs + + state["system_time_correct"] = self.system_time_correct + state["log_path"] = str(self.log_path) + + print("Available configs to send: ") + print(str(state["available_configs"])) + + print("Full state: ") + for key in state: + print("{} : {}".format(key, state[key])) + + self.socketio.emit("current state", state, namespace = "/test") + + + # this function reads satellite levels from an existing rtkrcv instance + # and emits them to the connected browser as messages + def broadcastSatellites(self): + count = 0 + + while self.server_not_interrupted: + + # update satellite levels + self.rtkc.getObs() + +# if count % 10 == 0: + #print("Sending sat rover levels:\n" + str(self.rtkc.obs_rover)) + #print("Sending sat base levels:\n" + str(self.rtkc.obs_base)) + + self.socketio.emit("satellite broadcast rover", self.rtkc.obs_rover, namespace = "/test") + #self.socketio.emit("satellite broadcast base", self.rtkc.obs_base, namespace = "/test") + count += 1 + self.sleep_count +=1 + time.sleep(1) + #print("exiting satellite broadcast") + + # this function reads current rtklib status, coordinates and obs count + def broadcastCoordinates(self): + count = 0 + + while self.server_not_interrupted: + + # update RTKLIB status + self.rtkc.getStatus() + +# if count % 10 == 0: +# print("Sending RTKLIB status select information:") +# print(self.rtkc.status) + + self.socketio.emit("coordinate broadcast", self.rtkc.status, namespace = "/test") + +# if self.enable_led: +# self.updateLED() + + count += 1 + time.sleep(1) + #print("exiting coordinate broadcast") diff --git a/web_app/RtkController.py b/web_app/RtkController.py new file mode 100644 index 00000000..1f80307e --- /dev/null +++ b/web_app/RtkController.py @@ -0,0 +1,301 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import os +import time +import signal +import pexpect +from threading import Semaphore, Thread + +# This module automates working with RTKRCV directly +# You can get sat levels, current status, start and restart the software + +class RtkController: + + def __init__(self, rtklib_path, config_path): + + self.bin_path = rtklib_path + self.config_path = config_path + + self.child = 0 + + self.status = {} + self.obs_rover = {} + self.obs_base = {} + self.info = {} + self.semaphore = Semaphore() + + self.started = False + self.launched = False + self.current_config = "" + + def expectAnswer(self, last_command = ""): + a = self.child.expect(["rtkrcv>", pexpect.EOF, "error"]) + # check rtklib output for any errors + if a == 1: + print("got EOF while waiting for rtkrcv> . Shutting down") + print("This means something went wrong and rtkrcv just stopped") + print("output before exception: " + str(self.child)) + return -1 + + if a == 2: + print("Could not " + last_command + ". Please check path to binary or config name") + print("You may also check serial port for availability") + return -2 + + return 1 + + def launch(self, config_name = None): + # run an rtkrcv instance with the specified config: + # if there is a slash in the name we consider it a full location + # otherwise, it's supposed to be in the upper directory(rtkrcv inside app) + + if config_name is None: + config_name = "rtkbase_single_default.conf" + + if not self.launched: + + self.semaphore.acquire() + + if "/" in config_name: + spawn_command = self.bin_path + "/rtkrcv -o " + config_name + else: + spawn_command = self.bin_path + "/rtkrcv -o " + os.path.join(self.config_path, config_name) + + self.child = pexpect.spawn(spawn_command, cwd = self.bin_path, echo = False) + + print('Launching rtklib with: "' + spawn_command + '"') + + if self.expectAnswer("spawn") < 0: + self.semaphore.release() + return -1 + + self.semaphore.release() + self.launched = True + self.current_config = config_name + + # launch success + return 1 + + # already launched + return 2 + + def shutdown(self): + + if self.launched: + self.semaphore.acquire() + + self.child.kill(signal.SIGUSR2) + + # wait for rtkrcv to shutdown + try: + self.child.wait() + except pexpect.ExceptionPexpect: + print("Already dead!!") + + if self.child.isalive(): + r = -1 + else: + r = 1 + + self.semaphore.release() + self.launched = False + + return r + + # already shut down + return 2 + + + def start(self): + + if not self.started: + self.semaphore.acquire() + + self.child.send("start\r\n") + + if self.expectAnswer("start") < 0: + self.semaphore.release() + return -1 + + self.semaphore.release() + self.started = True + + self.restart() + print("Restart") + return 1 + + # already started + return 2 + + def stop(self): + + if self.started: + self.semaphore.acquire() + + self.child.send("stop\r\n") + + if self.expectAnswer("stop") < 0: + self.semaphore.release() + return -1 + + self.semaphore.release() + + self.started = False + + return 1 + + # already stopped + return 2 + + def restart(self): + + if self.started: + self.semaphore.acquire() + + self.child.send("restart\r\n") + + if self.expectAnswer("restart") < 0: + self.semaphore.release() + return -1 + + self.semaphore.release() + + return 3 + else: + # if we are not started yet, just start + return self.start() + + def loadConfig(self, config_name = "rtk.conf"): + + self.semaphore.acquire() + + if "/" not in config_name: + # we assume this is not the full path + # so it must be in the upper dir + self.child.send("load " + "../" + config_name + "\r\n") + else: + self.child.send("load " + config_name + "\r\n") + + if self.expectAnswer("load config") < 0: + self.semaphore.release() + return -1 + + self.semaphore.release() + + self.current_config = config_name + + return 1 + + def getStatus(self): + + self.semaphore.acquire() + + self.child.send("status\r\n") + + if self.expectAnswer("get status") < 0: + self.semaphore.release() + return -1 + + status = self.child.before.decode().split("\r\n") + + if status != {}: + for line in status: + spl = line.split(":", 1) + + if len(spl) > 1: + + param = spl[0].strip() + value = spl[1].strip() + + self.status[param] = value + + self.semaphore.release() + + return 1 + + def getObs(self): + + self.semaphore.acquire() + + self.obs_rover = {} + self.obs_base = {} + + self.child.send("obs\r\n") + + if self.expectAnswer("get obs") < 0: + self.semaphore.release() + return -1 + + obs = self.child.before.decode().split("\r\n") + obs = [_f for _f in obs if _f] + + matching_strings = [s for s in obs if "SAT" in s] + + if matching_strings != []: + # find the header of the OBS table + header_index = obs.index(matching_strings[0]) + + # split the header string into columns + header = obs[header_index].split() + + if "S1" in header: + # find the indexes of the needed columns + sat_name_index = header.index("SAT") + sat_level_index = header.index("S1") + sat_input_source_index = header.index("R") + + if len(obs) > (header_index + 1): + # we have some info about the actual satellites: + + self.obs_rover = {} + self.obs_base = {} + + for line in obs[header_index+1:]: + spl = line.split() + + if len(spl) > sat_level_index: + name = spl[sat_name_index] + level = spl[sat_level_index] + + # R parameter corresponds to the input source number + if spl[sat_input_source_index] == "1": + # we consider 1 to be rover, + self.obs_rover[name] = level + elif spl[sat_input_source_index] == "2": + # 2 to be base + self.obs_base[name] = level + + else: + self.obs_base = {} + self.obs_rover = {} + + self.semaphore.release() + + return 1 + + + + + + diff --git a/web_app/ServiceController.py b/web_app/ServiceController.py new file mode 100644 index 00000000..5418db85 --- /dev/null +++ b/web_app/ServiceController.py @@ -0,0 +1,42 @@ +import os +from pystemd.systemd1 import Unit +from pystemd.systemd1 import Manager + +class ServiceController(object): + """ + A simple wrapper around pystemd to manage systemd services + """ + + manager = Manager(_autoload=True) + + def __init__(self, unit): + """ + param: unit: a systemd unit name (ie str2str_tcp.service...) + """ + self.unit = Unit(bytes(unit, 'utf-8'), _autoload=True) + + def isActive(self): + if self.unit.Unit.ActiveState == b'active': + return True + elif self.unit.Unit.ActiveState == b'activating': + #TODO manage this transitionnal state differently + return True + else: + return False + + def getUser(self): + return self.unit.Service.User.decode() + + def status(self): + return (self.unit.Unit.SubState).decode() + + def start(self): + self.manager.Manager.EnableUnitFiles(self.unit.Unit.Names, False, True) + return self.unit.Unit.Start(b'replace') + + def stop(self): + self.manager.Manager.DisableUnitFiles(self.unit.Unit.Names, False) + return self.unit.Unit.Stop(b'replace') + + def restart(self): + return self.unit.Unit.Restart(b'replace') \ No newline at end of file diff --git a/web_app/Str2StrController.py b/web_app/Str2StrController.py new file mode 100644 index 00000000..7f35a1a4 --- /dev/null +++ b/web_app/Str2StrController.py @@ -0,0 +1,300 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import os +import signal +import pexpect +from glob import glob + +from reach_tools import reach_tools + +# This module automates working with STR2STR software + +class Str2StrController: + + def __init__(self, rtklib_path): + + self.bin_path = rtklib_path + + self.gps_cmd_file_path = rtklib_path + "/app/rtkrcv" + self.gps_cmd_file = "GPS_10Hz.cmd" + + self.child = 0 + self.started = False + + # port settings are kept as class properties: + self.input_stream = "" + self.output_stream = "" + + # Reach defaults for base position and rtcm3 messages: + self.rtcm3_messages = ["1002", "1006", "1008", "1010", "1019", "1020"] + self.base_position = [] # lat, lon, height + + self.setSerialStream() # input ublox serial + #self.setTCPClientStream() + self.setTCPServerStream(input = False) # output tcp server on port 9000 + + def getAvailableReceiverCommandFiles(self): + # returns a list of available cmd files in the working rtkrcv directory + available_cmd_files = glob(self.gps_cmd_file_path + "/" +"*.cmd") + available_cmd_files = [os.path.basename(cmd_file) for cmd_file in available_cmd_files] + + return available_cmd_files + + def formCommentString(self, options_list): + + comment = "(" + + for index, option in enumerate(options_list): + comment += str(index) + ":" + str(option) + + if index < len(options_list) - 1: + comment += "," + + comment += ")" + + return comment + + def readConfig(self): + parameters_to_send = {} + + parameters_to_send["0"] = { + "parameter": "outstr-path", + "value": self.output_stream, + "comment": self.formCommentString(reach_tools.getAvailableSerialPorts()), + "description": "Output path for corrections" + } + + parameters_to_send["1"] = {"parameter": "rtcm3_out_messages", "value": ",".join(self.rtcm3_messages), "description": "RTCM3 messages for output"} + + # if we don't have a set base position we want to send empty strings + if not self.base_position: + base_pos = ["", "", ""] + else: + base_pos = self.base_position + + parameters_to_send["2"] = {"parameter": "base_pos_lat", "value": base_pos[0], "description": "Base latitude"} + parameters_to_send["3"] = {"parameter": "base_pos_lon", "value": base_pos[1], "description": "Base longitude"} + parameters_to_send["4"] = {"parameter": "base_pos_height", "value": base_pos[2], "description": "Base height"} + + parameters_to_send["5"] = { + "parameter": "gps_cmd_file", + "value": self.gps_cmd_file, + "description": "Receiver configuration file", + "comment": self.formCommentString(self.getAvailableReceiverCommandFiles()) + } + + print("DEBUG read") + print(parameters_to_send) + + return parameters_to_send + + def writeConfig(self, parameters_received): + + print("DEBUG write") + + print(parameters_received) + + coordinate_filled_flag = 3 + base_pos = [] + + self.output_stream = parameters_received["0"]["value"] + + # llh + self.base_position = [] + self.base_position.append(parameters_received["2"]["value"]) + self.base_position.append(parameters_received["3"]["value"]) + self.base_position.append(parameters_received["4"]["value"]) + + self.rtcm3_messages = parameters_received["1"]["value"].split(",") + + self.gps_cmd_file = parameters_received["5"]["value"] + + def setPort(self, port, input = True, format = "ubx"): + if input: + self.input_stream = port + "#" + format + else: + # str2str only supports rtcm3 for output + self.output_stream = port + "#" + "rtcm3" + + def setSerialStream(self, serial_parameters = None, input = True, format = "ubx"): + # easier way to specify serial port for str2str + # serial_parameters is a list of options for our serial device: + # 1. serial port + # 2. baudrate + # 3. byte size + # 4. parity bit + # 5. stop bit + # 6. fctr + # default parameters here are Reach standards + + def_parameters = [ + "ttyACM0", + "230400", + "8", + "n", + "1", + "off" + ] + + if serial_parameters is None: + serial_parameters = def_parameters + + port = "serial://" + ":".join(serial_parameters) + + self.setPort(port, input, format) + + def setTCPClientStream(self, tcp_client_parameters = None, input = True, format = "ubx"): + # easier way to specify tcp connection parameters for str2str + # tcp client parameters include: + # 1. ip address + # 2. port number + + def_parameters = [ + "localhost", + "5015" + ] + + if tcp_client_parameters is None: + tcp_client_parameters = def_parameters + + port = "tcpcli://" + ":".join(tcp_server_parameters) + + self.setPort(port, input, format) + + def setTCPServerStream(self, tcp_server_parameters = None, input = True, format = "ubx"): + # tcp server parameters only include the port number: + # 1. port number + + def_parameters = [ + "9000" + ] + + if tcp_server_parameters is None: + tcp_server_parameters = def_parameters + + port = "tcpsvr://:" + def_parameters[0] + + self.setPort(port, input, format) + + def setNTRIPClientStream(self, ntrip_client_parameters = None, input = True, format = "ubx"): + # ntrip client parameters: + # 1. user + # 2. password + # 3. address + # 4. port + # 5. mount point + + port = "ntrip://" + ntrip_client_parameters[0] + ":" + port += ntrip_client_parameters[1] + "@" + ntrip_client_parameters[2] + ":" + port += ntrip_client_parameters[3] + "/" + ntrip_client_parameters[4] + + self.setPort(port, input, format) + + def setNTRIPServerStream(self, ntrip_server_parameters = None, input = True, format = "ubx"): + # ntrip client parameters: + # 1. password + # 2. address + # 3. port + # 4. mount point + # 5. str ??? + + port = "ntrips://:" + ntrip_client_parameters[0] + "@" + ntrip_client_parameters[1] + port += ":" + ntrip_client_parameters[2] + "/" + ntrip_client_parameters[3] + ":" + port += ntrip_client_parameters[4] + + self.setPort(port, input, format) + + def start(self, rtcm3_messages = None, base_position = None, gps_cmd_file = None): + # when we start str2str we also have 3 important optional parameters + # 1. rtcm3 message types. We have standard 1002, 1006, 1013, 1019 by default + # 2. base position in llh. By default we don't pass any values, however it is best to use this feature + # 3. gps cmd file will take care of msg frequency and msg types + # To pass parameters to this function use string lists, like ["1002", "1006"] or ["60", "30", "100"] + + print(self.bin_path) + + if not self.started: + if rtcm3_messages is None: + rtcm3_messages = self.rtcm3_messages + + if base_position is None: + base_position = self.base_position + + if gps_cmd_file is None: + gps_cmd_file = self.gps_cmd_file + + cmd = "/str2str -in " + self.input_stream + " -out " + self.output_stream + " -msg " + ",".join(rtcm3_messages) + + if "" in base_position: + base_position = [] + + if base_position: + cmd += " -p " + " ".join(base_position) + + if gps_cmd_file: + cmd += " -c " + self.gps_cmd_file_path + "/" + gps_cmd_file + + cmd = self.bin_path + cmd + print("Starting str2str with") + print(cmd) + + self.child = pexpect.spawn(cmd, cwd = self.bin_path, echo = False) + + a = self.child.expect(["stream server start", pexpect.EOF, "error"]) + # check if we encountered any errors launching str2str + if a == 1: + print("got EOF while waiting for stream start. Shutting down") + print("This means something went wrong and str2str just stopped") + print("output before exception: " + str(self.child)) + return -1 + + if a == 2: + print("Could not start str2str. Please check path to binary or parameters, like serial port") + print("You may also check serial, tcp, ntrip ports for availability") + return -2 + + # if we are here, everything is good + self.started = True + return 1 + + # str2str already started + return 2 + + def stop(self): + # terminate the stream + + if self.started: + self.child.kill(signal.SIGUSR2) + try: + self.child.wait() + except pexpect.ExceptionPexpect: + print("Str2str already down") + + self.started = False + return 1 + + # str2str already stopped + return 2 + + diff --git a/unit/test.service b/web_app/log_converter/__init__.py similarity index 100% rename from unit/test.service rename to web_app/log_converter/__init__.py diff --git a/web_app/log_converter/convbin.py b/web_app/log_converter/convbin.py new file mode 100644 index 00000000..03f5fe3d --- /dev/null +++ b/web_app/log_converter/convbin.py @@ -0,0 +1,118 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import pexpect +from .logs import Log, LogMetadata + +class Convbin: + + supported_log_formats = ["rtcm2", "rtcm3", "nov", "oem3", "ubx", "ss2", "hemis", "stq", "javad", "nvs", "binex", "rinex"] + + def __init__(self, rtklib_path): + self.bin_path = rtklib_path + self.child = 0 + + def convertRTKLIBLogToRINEX(self, log_path, rinex_version="3.01"): + + print("Converting log " + log_path + "...") + + result = None + + # check if extension format is in the list of the supported ones + log_format = [f for f in self.supported_log_formats if log_path.endswith(f)] + + if log_format: + try: + log_metadata = self.convertLogToRINEX(log_path, log_format[0], rinex_version) + except ValueError: + return None + + if log_metadata: + result = Log(log_path, log_metadata) + + return result + + def convertLogToRINEX(self, log_path, format, rinex_version): + + spawn_command = " ".join([ + self.bin_path + "/convbin", + "-r", + format, + "-v", + rinex_version, + "-od", + "-os", + "-oi", + "-ot", + "-ol", + log_path + ]) + + print("Specified format is " + format) + + print("Spawning convbin with " + spawn_command) + self.child = pexpect.spawn(spawn_command, echo = False) + print("Process spawned!") + self.child.expect(pexpect.EOF, timeout = None) + + if self.child.exitstatus != 0 and self.child.signalstatus == None: + print("Convbin killed by external signal") + raise ValueError + + print("Conversion process finished correctly") + return self.parseConvbinOutput(self.child.before) + + def parseConvbinOutput(self, output): + + result_string = self.extractResultingString(output) + + if self.resultStringIsValid(result_string): + return LogMetadata(result_string) + else: + return None + + def resultStringIsValid(self, result_string): + return True if len(result_string) > 21 else False + + def extractResultingString(self, output): + # get the last line of the convbin output + + last_line_end = output.rfind("\r\r\n") + last_line_start = output.rfind("\r", 0, last_line_end) + 1 + + return output[last_line_start:last_line_end] + + +if __name__ == "__main__": + cb = Convbin("/home/reach/RTKLIB") + rlog = cb.convertRTKLIBLogToRINEX("/home/reach/logs/rov_201601210734.ubx") + print(rlog) + # print("base") + # blog = cb.convertRTKLIBLogToRINEX("/home/egor/RTK/convbin_test/ref_201601080935.rtcm3") + # print(blog) + # print("Kinelog") + # kinelog = KinematicLog(rlog, blog) + # print(kinelog) + # kinelog.createKinematicLogPackage("lol.zip") + # rlog.createLogPackage("lol1.zip") + diff --git a/web_app/log_converter/logs.py b/web_app/log_converter/logs.py new file mode 100644 index 00000000..39528255 --- /dev/null +++ b/web_app/log_converter/logs.py @@ -0,0 +1,239 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import glob +import zipfile +import os + +class LogMetadata: + + message_names = { + "OBS": "Obs", + "NAV": "GPS nav", + "GNAV": "GLONASS nav", + "HNAV": "GEO nav", + "QNAV": "QZSS nav", + "LNAV": "Galileo nav", + "SBAS": "SBAS log", + "Errors": "Errors", + "TM": "Time marks" + } + + def __init__(self, convbin_output): + + self.start_timestamp = 0 + self.stop_timestamp = 0 + self.navigation_messages = {msg_type: 0 for msg_type in list(self.message_names.keys())} + + self.extractDataFromString(convbin_output) + + def __str__(self): + to_print = "Log start time: " + self.formatTimestamp(self.start_timestamp) + "\n" + to_print += "Log stop time: " + self.formatTimestamp(self.stop_timestamp) + "\n" + to_print += "Navigation messages parsed:\n" + to_print += self.formValidMessagesString() + + return to_print + + def formatTimestamp(self, timestamp): + # 19800106000000 + timestamp = str(timestamp) + + # date + human_readable_timestamp = timestamp[:4] + "-" + timestamp[4:6] + "-" + timestamp[6:8] + # time + human_readable_timestamp += " " + timestamp[8:10] + human_readable_timestamp += ":" + timestamp[10:12] + human_readable_timestamp += ":" + timestamp[12:14] + + return human_readable_timestamp + + def countValidMessages(self): + + valid_messages = 0 + + for msg_type, msg_count in list(self.navigation_messages.items()): + if msg_type is not "Errors": + valid_messages += int(msg_count) + + return valid_messages + + def formValidMessagesString(self): + + correct_order = list(self.message_names.keys()) + + to_print = "Messages inside: " + + for msg in correct_order: + msg_type = msg + msg_count = self.navigation_messages[msg_type] + if int(msg_count) > 0: + to_print += self.message_names[msg_type] + ": " + msg_count + ", " + + return to_print[:-2] + + def extractDataFromString(self, data_string): + # example string: + # 2016/01/08 09:35:02-01/08 11:24:58: O=32977 N=31 G=41 E=2 + + data_list = data_string.split(" ") + data_list = [_f for _f in data_list if _f] + + # first 3 parts mark the time properties + # the next elemets show message counts + + self.extractTimeDataFromString(data_list[:3]) + self.extractMessageCountFromString(data_list[3:]) + + def extractTimeDataFromString(self, data_list): + # example string(split into a list by spaces) + # 2016/01/08 09:35:02-01/08 11:24:58: + + # remove all the extra punctuation + raw_data = "".join(data_list) + raw_data = raw_data.translate(None, "/:\r") + + print("Raw data is " + raw_data) + start_timestamp, stop_timestamp = raw_data.split("-") + stop_year = self.calculateStopYear(start_timestamp, stop_timestamp) + + self.start_timestamp = start_timestamp + self.stop_timestamp = stop_year + stop_timestamp + + def calculateStopYear(self, start_timestamp, stop_timestamp): + # calc stop year for the stop timestamp + + start_year = int(start_timestamp[:4]) + start_month = int(start_timestamp[4:6]) + + stop_month = int(stop_timestamp[0:2]) + + # we assume logs can't last longer than a year + stop_year = start_year if start_month <= stop_month else start_year + 1 + + return str(stop_year) + + def extractMessageCountFromString(self, data_list): + # example string(split into a list by spaces) + # O=32977 N=31 G=41 E=2 + + msg_dictionary = {msg_type[0]: msg_type for msg_type in list(self.message_names.keys())} + + for entry in data_list: + split_entry = entry.split("=") + msg_type = msg_dictionary[split_entry[0]] + msg_count = split_entry[1] + + # append the resulting data + self.navigation_messages[msg_type] = msg_count + + +class Log: + + rinex_file_extensions = [".obs", ".nav", ".gnav", ".hnav", ".qnav", ".lnav", ".sbs"] + + def __init__(self, log_path, log_metadata): + + self.log_path = log_path + self.log_name = os.path.splitext(os.path.basename(self.log_path))[0] + + self.log_metadata = log_metadata + + self.RINEX_files = self.findRINEXFiles(os.path.dirname(self.log_path)) + + self.log_package_path = "" + + def __str__(self): + + to_print = "Printing log info:\n" + to_print += "Full path to log == " + self.log_path + "\n" + to_print += "Available RINEX files: " + to_print += str(self.RINEX_files) + "\n" + to_print += str(self.log_metadata) + "\n" + to_print += "ZIP file layout:\n" + to_print += str(self.prepareLogPackage()) + + return to_print + + def isValid(self): + # determine whether the log has valuable RINEX info + return True if self.log_metadata.countValidMessages() else False + + def findRINEXFiles(self, log_directory): + + files_in_dir = glob.glob(log_directory + "/*") + rinex_files_in_dir = [] + + for f in files_in_dir: + filename = os.path.basename(f) + name, extension = os.path.splitext(filename) + + if extension in self.rinex_file_extensions: + if name == self.log_name: + rinex_files_in_dir.append(f) + + return rinex_files_in_dir + + def prepareLogPackage(self): + # return a list of tuples [("abspath", "wanted_path_inside_zip"), ... ] + # with raw and RINEX logs:w + + files_list = [] + # add raw log + files_list.append((self.log_path, "Raw/" + os.path.basename(self.log_path))) + + for rinex_file in self.RINEX_files: + rinex_files_paths = (rinex_file, "RINEX/" + os.path.basename(rinex_file)) + files_list.append(rinex_files_paths) + + return files_list + + def createLogPackage(self, package_destination=None): + # files_list is a list of tuples [("abspath", "wanted_path_inside_zip"), ... ] + + if package_destination is None: + package_destination = os.path.dirname(self.log_path) + "/" + self.log_name + ".zip" + file_tree = self.prepareLogPackage() + + with zipfile.ZipFile(package_destination, "w") as newzip: + for f in file_tree: + newzip.write(f[0], f[1]) + + newzip.writestr("readme.txt", str(self.log_metadata)) + + # delete unzipped files + self.deleteLogFiles() + + return package_destination + + def deleteLogFiles(self): + + all_log_files = self.RINEX_files + # all_log_files.append(self.log_path) + + for log in all_log_files: + try: + os.remove(log) + except OSError: + pass + diff --git a/web_app/port.py b/web_app/port.py new file mode 100644 index 00000000..94525903 --- /dev/null +++ b/web_app/port.py @@ -0,0 +1,48 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +from os import system + +def sh(script): + system("bash -c '%s'" % script) + +# change baudrate to 230400 +def br230400(): + cmd = ["echo", "-en", '"\\xb5\\x62\\x06\\x00\\x01\\x00\\x01\\x08\\x22\\xb5\\x62\\x06\\x00\\x14\\x00\\x01\\x00\\x00\\x00\\xd0\\x08\\x00\\x00\\x00\\x84\\x03\\x00\\x07\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x84\\xe8\\xb5\\x62\\x06\\x00\\x01\\x00\\x01\\x08\\x22"', ">", "/dev/ttyMFD1"] + cmd = " ".join(cmd) + sh(cmd) + +# change baudrate to 230400 from any previous baudrates +def changeBaudrateTo115200(): + # typical baudrate values +# br = ["4800", "9600", "19200", "38400", "57600", "115200", "230400"] + br = ["4800", "9600", "19200", "38400", "57600", "115200"] + cmd = ["stty", "-F", "/dev/ttyACM0"] + + for rate in br: + cmd.append(str(rate)) + cmd_line = " ".join(cmd) + sh(cmd_line) + +# br230400() +# cmd.pop() diff --git a/web_app/reach_tools/__init__.py b/web_app/reach_tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web_app/reach_tools/gps_time.py b/web_app/reach_tools/gps_time.py new file mode 100755 index 00000000..172cf572 --- /dev/null +++ b/web_app/reach_tools/gps_time.py @@ -0,0 +1,186 @@ +#!/usr/bin/python + +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import serial +import binascii +import ctypes +import subprocess +from . import reach_tools + +def hexify(char_list): + """ transform a char list into a list of int values""" + return [ord(c) for c in char_list] + +def enable_nav_timeutc(port): + poll_time_utc = ["b5", "62", "06", "01", "03", "00", "01", "21", "01", "2d", "85"] + msg = binascii.unhexlify("".join(poll_time_utc)) + port.write(msg) + +def time_synchronised_by_ntp(): + out = subprocess.check_output("timedatectl") + + if "NTP synchronized: yes" in out: + return True + else: + return False + +def update_system_time(date, time): + # requires a date list and a time list + # ["YYYY", "MM", "DD"], ["hh", "mm", "ss"] + print("##### UPDATING SYSTEM TIME #####") + print(date) + print(time) + # busybox date cmd can use a following format + # YYYY.MM.DD-hh:mm:ss + # real date -s needs YYYY-MM-DD hh:mm:ss + printable_date = "-".join(str(x) for x in date) + printable_time = ":".join(str(x) for x in time) + + datetime_string = printable_date + " " + printable_time + cmd = ["date", "-s", datetime_string] + out = subprocess.check_output(cmd) + +def get_gps_time(port): + + try: + multiple_bytes = port.read(1024) + except OSError: + print("Could not open serial device") + else: + ubx_log = hexify(multiple_bytes) + time_data = MSG_NAV_TIMEUTC(ubx_log) + print(time_data) + + if time_data.time_valid: + return time_data.date, time_data.time + + return None, None + +def set_gps_time(serial_device, baud_rate): + + port = serial.Serial(serial_device, baud_rate, timeout = 1.5) + enable_nav_timeutc(port) + + print("Restarting ntp service for faster sync...") + reach_tools.run_command_safely(["timedatectl", "set-ntp", "false"]) + reach_tools.run_command_safely(["timedatectl", "set-ntp", "true"]) + + print("TIMEUTC enabled") + time = None + ntp_not_synced = True + + while time is None and ntp_not_synced: + date, time = get_gps_time(port) + ntp_not_synced = not time_synchronised_by_ntp() + + if ntp_not_synced: + update_system_time(date, time) + +class MSG_NAV_TIMEUTC: + + msg_start = [0xb5, 0x62, 0x01, 0x21, 0x14, 0x00] + msg_length = 28 + + def __init__(self, ubx_hex_log): + self.time_valid = False + self.date = None + self.time = None + + extracted_messages = self.scan_log(ubx_hex_log) + + if extracted_messages: + for msg in extracted_messages: + if self.is_valid(msg): + if self.time_is_valid(msg): + self.time_valid = True + self.date, self.time= self.unpack(msg) + + def __str__(self): + to_print = "ubx NAV-TIMEUTC message\n" + + if self.time_valid: + to_print += "Time data is valid\n" + to_print += ".".join(str(x) for x in self.date) + to_print += " " + to_print += ":".join(str(x) for x in self.time) + else: + to_print += "Time data is invalid!" + + return to_print + + def scan_log(self, ubx_hex_log): + """Search the provided log for a required msg header""" + matches = [] + pattern = self.msg_start + msg_length = self.msg_length + + for i in range(0, len(ubx_hex_log)): + if ubx_hex_log[i] == pattern[0] and ubx_hex_log[i:i + len(pattern)] == pattern: + matches.append(ubx_hex_log[i:i + msg_length]) + + return matches + + def is_valid(self, msg): + """Count and verify the checksum of a ubx message. msg is a list of hex values""" + + to_check = msg[2:-2] + + ck_a = ctypes.c_uint8(0) + ck_b = ctypes.c_uint8(0) + + for num in to_check: + byte = ctypes.c_uint8(num) + ck_a.value = ck_a.value + byte.value + ck_b.value = ck_b.value + ck_a.value + + if (ck_a.value, ck_b.value) == (ctypes.c_uint8(msg[-2]).value, ctypes.c_uint8(msg[-1]).value): + return True + else: + return False + + def time_is_valid(self, msg): + """Check the flags confirming utc time in the message is valid""" + flag_byte = ctypes.c_uint8(msg[-3]) + return True if flag_byte.value & 4 == 4 else False + + def unpack(self, msg): + """Extract the actual time from the message""" + datetime = [] + + # unpack year + byte1 = ctypes.c_uint8(msg[18]) + byte2 = ctypes.c_uint8(msg[19]) + + year = ctypes.c_uint16(byte2.value << 8 | byte1.value).value + datetime.append(year) + # unpack month, day, hour, minute, second + for i in range(20, 25): + datetime.append(msg[i]) + + date = datetime[:3] + time = datetime[3:] + + return date, time + diff --git a/web_app/reach_tools/provisioner.py b/web_app/reach_tools/provisioner.py new file mode 100644 index 00000000..bf9c2d35 --- /dev/null +++ b/web_app/reach_tools/provisioner.py @@ -0,0 +1,148 @@ +#!/usr/bin/python + +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import pip +import subprocess +import os +from . import reach_tools +import imp +import shutil + +def install_pip_packages(): + + packages = [ + ("pybluez", "bluetooth") + ] + + for p in packages: + try: + imp.find_module(p[1]) + except ImportError: + print("No module " + p[0] + " found...") + pip.main(["install", p[0]]) + +def check_opkg_packages(packages): + + packages_to_check = packages + + try: + out = subprocess.check_output(["opkg", "list-installed"]) + except subprocess.CalledProcessError: + print("Error getting installed opkg packages") + return None + else: + for p in out.split("\n"): + if p: + installed_package_name = p.split()[0] + if installed_package_name in packages_to_check: + packages_to_check.remove(installed_package_name) + + return packages_to_check + +def install_opkg_packages(packages): + + packages = check_opkg_packages(packages) + + if packages: + print("Installing missing packages:") + print(packages) + try: + subprocess.check_output(["opkg", "update"]) + except subprocess.CalledProcessError: + print("No internet connection, so no package installs!") + pass + else: + for p in packages: + subprocess.check_output(["opkg", "install", p]) + +def restart_bt_daemon(): + reach_tools.run_command_safely(["rfkill", "unblock", "bluetooth"]) + reach_tools.run_command_safely(["systemctl", "daemon-reload"]) + reach_tools.run_command_safely(["systemctl", "restart", "bluetooth.service"]) + reach_tools.run_command_safely(["systemctl", "restart", "bluetooth.service"]) + reach_tools.run_command_safely(["hciconfig", "hci0", "reset"]) + +def enable_bt_compatibility(file_path): + + with open(file_path, "r") as f: + data_read = f.readlines() + + need_to_update = True + required_line = 0 + + for line in data_read: + if "ExecStart=/usr/lib/bluez5/bluetooth/bluetoothd -C" in line: + need_to_update = False + + if need_to_update: + data_to_write = [] + + for line in data_read: + if "ExecStart=/usr/lib/bluez5/bluetooth/bluetoothd" in line: + to_append = "ExecStart=/usr/lib/bluez5/bluetooth/bluetoothd -C\n" + else: + to_append = line + + data_to_write.append(to_append) + + with open(file_path, "w") as f: + f.writelines(data_to_write) + + reach_tools.run_command_safely(["sync"]) + + restart_bt_daemon() + +def update_bluetooth_service(): + first = "/lib/systemd/system/bluetooth.service" + second = "/etc/systemd/system/bluetooth.target.wants/bluetooth.service" + enable_bt_compatibility(first) + enable_bt_compatibility(second) + restart_bt_daemon() + +def check_RTKLIB_integrity(): + RTKLIB_path = "/home/reach/RTKLIB/" + reachview_binaries_path = "/home/reach/rtklib_configs/" + + RTKLIB_binaries = [ + (RTKLIB_path + "app/rtkrcv/gcc/rtkrcv", reachview_binaries_path + "rtkrcv"), + (RTKLIB_path + "app/convbin/gcc/convbin", reachview_binaries_path + "convbin"), + (RTKLIB_path + "app/str2str/gcc/str2str", reachview_binaries_path + "str2str") + ] + + for b in RTKLIB_binaries: + if not os.path.isfile(b[0]): + print("Could not find " + b[0] + "! Copying from ReachView backup...") + shutil.copy(b[1], b[0]) + +def provision_reach(): + install_pip_packages() + packages = ["kernel-module-ftdi-sio"] + install_opkg_packages(packages) + update_bluetooth_service() + check_RTKLIB_integrity() + +if __name__ == "__main__": + provision_reach() + diff --git a/web_app/reach_tools/reach_tools.py b/web_app/reach_tools/reach_tools.py new file mode 100644 index 00000000..244aa0b3 --- /dev/null +++ b/web_app/reach_tools/reach_tools.py @@ -0,0 +1,171 @@ +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +import os +import subprocess + +def getImageVersion(): + + image_version_file = "/home/pi/.reach/image_version" + + try: + with open(image_version_file, "r") as f: + image_version = f.readline().rstrip("\n") + except (IOError, OSError): + print("Could not find version file inside system") + print("This is image v1.0") + image_version = "v1.0" + + return image_version + +def getNetworkStatus(): + + # get Wi-Fi mode, Master or Managed +# cmd = ["configure_edison", "--showWiFiMode"] +# cmd = " ".join(cmd) +# mode = subprocess.check_output(cmd, shell = True).strip() + +# mode = "Managed" +# ssid = "empty" +# ip_address = "empty" + + mode = "Master" + ssid = "RTK_test" + ip_address = "192.168.43.1" + +# if mode == "Managed": + # we are in managed, client mode + # we can extract all information from "wpa_cli status" + +# cmd = ["wpa_cli", "status"] +# cmd = " ".join(cmd) +# out = subprocess.check_output(cmd, shell = True) + +# out = out.split("\n") + +# for line in out: +# if "ssid=" in line: +# ssid = line[5:] +# if "ip_address=" in line: +# ip_address = line[11:] + +# if mode == "Master": + # we are in master, AP mode + # we can extract all info from "configure_edison" + # with differnet parameters + + # example of the output {"hostname": "reach", "ssid": "reach:ec:e8", "default_ssid": "edison_ap"} +# cmd = ["configure_edison", "--showNames"] +# cmd = " ".join(cmd) +# out = subprocess.check_output(cmd, shell = True) + +# anchor = '"ssid": "' + +# ssid_start_position = out.find(anchor) + len(anchor) +# ssid_stop_position = out.find('"', ssid_start_position) + +# ssid = out[ssid_start_position:ssid_stop_position] + +# cmd = ["configure_edison", "--showWiFiIP"] +# cmd = " ".join(cmd) +# ip_address = subprocess.check_output(cmd, shell = True).strip() + + return {"mode": mode, "ssid": ssid, "ip_address": ip_address} + +def getAppVersion(): + # Extract git tag as software version +# git_tag_cmd = "git describe --tags" +# app_version = subprocess.check_output([git_tag_cmd], shell = True, cwd = "/home/reach") + app_version = 1.0 + + return app_version + +def getSystemStatus(): + + system_status = { + "network_status": getNetworkStatus(), + "image_version": getImageVersion(), + "app_version": getAppVersion(), + } + + return system_status + +def getAvailableSerialPorts(): + + possible_ports_ports_to_use = ["ttyACM0", "ttyUSB0"] + serial_ports_to_use = [port for port in possible_ports_ports_to_use if os.path.exists("/dev/" + port)] + + return serial_ports_to_use + +def getLogsSize(logs_path): + #logs_path = "/home/pi/logs/" + size_in_bytes = sum(os.path.getsize(logs_path + f) for f in os.listdir(logs_path) if os.path.isfile(logs_path + f)) + return size_in_bytes/(1024*1024) + +def getFreeSpace(logs_path): + space = os.statvfs(os.path.expanduser("~")) + free = space.f_bavail * space.f_frsize / 1024000 + total = space.f_blocks * space.f_frsize / 1024000 + + used_by_logs = getLogsSize(logs_path) + total_for_logs = free + used_by_logs + percentage = (float(used_by_logs)/float(total_for_logs)) * 100 + total_for_logs_gb = float(total_for_logs) / 1024.0 + + result = { + "used": "{0:.0f}".format(used_by_logs), + "total": "{0:.1f}".format(total_for_logs_gb), + "percentage": "{0:.0f}".format(percentage) + } + + print("Returning sizes!") + print(result) + + return result + +def run_command_safely(cmd): + try: + out = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + out = None + + return out + + + + + + + + + + + + + + + + + + + diff --git a/web_app/requirements.txt b/web_app/requirements.txt new file mode 100644 index 00000000..c975d527 --- /dev/null +++ b/web_app/requirements.txt @@ -0,0 +1,13 @@ +cryptography +Flask +Flask-SocketIO +eventlet +Bootstrap-Flask +Flask-WTF +Flask-Login +pexpect +pyOpenSSL +pyserial +pystemd +requests + diff --git a/web_app/rtklib_configs/Readme b/web_app/rtklib_configs/Readme new file mode 100644 index 00000000..5020074d --- /dev/null +++ b/web_app/rtklib_configs/Readme @@ -0,0 +1 @@ +Default configs files for rtklib diff --git a/web_app/rtklib_configs/rtkbase_single_default.conf b/web_app/rtklib_configs/rtkbase_single_default.conf new file mode 100644 index 00000000..b330ea43 --- /dev/null +++ b/web_app/rtklib_configs/rtkbase_single_default.conf @@ -0,0 +1,159 @@ +# RTKNAVI options (2020/03/31 19:18:18, v.demo5 b33a) + +pos1-posmode =ppp-static # (0:single,1:dgps,2:kinematic,3:static,4:static-start,5:movingbase,6:fixed,7:ppp-kine,8:ppp-static,9:ppp-fixed) +pos1-frequency =l1+l2+l5 # (1:l1,2:l1+l2,3:l1+l2+l5,4:l1+l2+l5+l6,2:l1+l2+e5b) +pos1-soltype =forward # (0:forward,1:backward,2:combined) +pos1-elmask =15 # (deg) +pos1-snrmask_r =off # (0:off,1:on) +pos1-snrmask_b =off # (0:off,1:on) +pos1-snrmask_L1 =0,0,0,0,0,0,0,0,0 +pos1-snrmask_L2 =0,0,0,0,0,0,0,0,0 +pos1-snrmask_L5 =0,0,0,0,0,0,0,0,0 +pos1-dynamics =on # (0:off,1:on) +pos1-tidecorr =off # (0:off,1:on,2:otl) +pos1-ionoopt =brdc # (0:off,1:brdc,2:sbas,3:dual-freq,4:est-stec,5:ionex-tec,6:qzs-brdc,7:qzs-lex,8:stec) +pos1-tropopt =saas # (0:off,1:saas,2:sbas,3:est-ztd,4:est-ztdgrad,5:ztd) +pos1-sateph =brdc # (0:brdc,1:precise,2:brdc+sbas,3:brdc+ssrapc,4:brdc+ssrcom) +pos1-posopt1 =on # (0:off,1:on) +pos1-posopt2 =on # (0:off,1:on) +pos1-posopt3 =on # (0:off,1:on,2:precise) +pos1-posopt4 =on # (0:off,1:on) +pos1-posopt5 =off # (0:off,1:on) +pos1-posopt6 =off # (0:off,1:on) +pos1-exclsats = # (prn ...) +pos1-navsys =63 # (1:gps+2:sbas+4:glo+8:gal+16:qzs+32:comp) +pos2-armode =continuous # (0:off,1:continuous,2:instantaneous,3:fix-and-hold) +pos2-gloarmode =fix-and-hold # (0:off,1:on,2:autocal,3:fix-and-hold) +pos2-bdsarmode =on # (0:off,1:on) +pos2-arfilter =on # (0:off,1:on) +pos2-arthres =3 +pos2-arthres1 =0.1 +pos2-arthres2 =0 +pos2-arthres3 =1e-09 +pos2-arthres4 =1e-05 +pos2-varholdamb =0.1 # (cyc^2) +pos2-gainholdamb =0.01 +pos2-arlockcnt =0 +pos2-minfixsats =4 +pos2-minholdsats =5 +pos2-mindropsats =10 +pos2-rcvstds =off # (0:off,1:on) +pos2-arelmask =15 # (deg) +pos2-arminfix =20 +pos2-armaxiter =1 +pos2-elmaskhold =15 # (deg) +pos2-aroutcnt =20 +pos2-maxage =30 # (s) +pos2-syncsol =off # (0:off,1:on) +pos2-slipthres =0.05 # (m) +pos2-rejionno =1000 # (m) +pos2-rejgdop =30 +pos2-niter =1 +pos2-baselen =0 # (m) +pos2-basesig =0 # (m) +out-solformat =llh # (0:llh,1:xyz,2:enu,3:nmea) +out-outhead =on # (0:off,1:on) +out-outopt =on # (0:off,1:on) +out-outvel =off # (0:off,1:on) +out-timesys =gpst # (0:gpst,1:utc,2:jst) +out-timeform =hms # (0:tow,1:hms) +out-timendec =3 +out-degform =deg # (0:deg,1:dms) +out-fieldsep = +out-outsingle =off # (0:off,1:on) +out-maxsolstd =0 # (m) +out-height =ellipsoidal # (0:ellipsoidal,1:geodetic) +out-geoid =internal # (0:internal,1:egm96,2:egm08_2.5,3:egm08_1,4:gsi2000) +out-solstatic =all # (0:all,1:single) +out-nmeaintv1 =0 # (s) +out-nmeaintv2 =0 # (s) +out-outstat =residual # (0:off,1:state,2:residual) +stats-weightmode =elevation # (0:elevation,1:snr) +stats-eratio1 =300 +stats-eratio2 =300 +stats-eratio5 =300 +stats-errphase =0.003 # (m) +stats-errphaseel =0.003 # (m) +stats-errphasebl =0 # (m/10km) +stats-errdoppler =1 # (Hz) +stats-snrmax =52 # (dB.Hz) +stats-stdbias =30 # (m) +stats-stdiono =0.03 # (m) +stats-stdtrop =0.3 # (m) +stats-prnaccelh =3 # (m/s^2) +stats-prnaccelv =1 # (m/s^2) +stats-prnbias =0.0001 # (m) +stats-prniono =0.001 # (m) +stats-prntrop =0.0001 # (m) +stats-prnpos =0 # (m) +stats-clkstab =5e-12 # (s/s) +ant1-postype =llh # (0:llh,1:xyz,2:single,3:posfile,4:rinexhead,5:rtcm,6:raw) +ant1-pos1 =90 # (deg|m) +ant1-pos2 =0 # (deg|m) +ant1-pos3 =-6335367.6285 # (m|m) +ant1-anttype = +ant1-antdele =0 # (m) +ant1-antdeln =0 # (m) +ant1-antdelu =0 # (m) +ant2-postype =rtcm # (0:llh,1:xyz,2:single,3:posfile,4:rinexhead,5:rtcm,6:raw) +ant2-pos1 =0 # (deg|m) +ant2-pos2 =0 # (deg|m) +ant2-pos3 =0 # (m|m) +ant2-anttype = +ant2-antdele =0 # (m) +ant2-antdeln =0 # (m) +ant2-antdelu =0 # (m) +ant2-maxaveep =1 +ant2-initrst =on # (0:off,1:on) +misc-timeinterp =off # (0:off,1:on) +misc-sbasatsel =0 # (0:all) +misc-rnxopt1 = +misc-rnxopt2 = +misc-pppopt = +file-satantfile = +file-rcvantfile = +file-staposfile = +file-geoidfile = +file-ionofile = +file-dcbfile = +file-eopfile = +file-blqfile = +file-tempdir = +file-geexefile = +file-solstatfile = +file-tracefile = +# + +inpstr1-type =tcpcli # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,7:ntripcli,8:ftp,9:http) +inpstr2-type =off # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,7:ntripcli,8:ftp,9:http) +inpstr3-type =off # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,7:ntripcli,8:ftp,9:http) +inpstr1-path =127.0.0.1:5015 +inpstr2-path = +inpstr3-path = +inpstr1-format =ubx # (0:rtcm2,1:rtcm3,2:oem4,3:oem3,4:ubx,5:swift,6:hemis,7:skytraq,8:gw10,9:javad,10:nvs,11:binex,12:rt17,13:sbf,14:cmr,15:tersus,17:sp3) +inpstr2-format =rtcm3 # (0:rtcm2,1:rtcm3,2:oem4,3:oem3,4:ubx,5:swift,6:hemis,7:skytraq,8:gw10,9:javad,10:nvs,11:binex,12:rt17,13:sbf,14:cmr,15:tersus,17:sp3) +inpstr3-format =rtcm2 # (0:rtcm2,1:rtcm3,2:oem4,3:oem3,4:ubx,5:swift,6:hemis,7:skytraq,8:gw10,9:javad,10:nvs,11:binex,12:rt17,13:sbf,14:cmr,15:tersus,17:sp3) +inpstr2-nmeareq =off # (0:off,1:latlon,2:single) +inpstr2-nmealat =0 # (deg) +inpstr2-nmealon =0 # (deg) +outstr1-type =off # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,6:ntripsvr) +outstr2-type =off # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,6:ntripsvr) +outstr1-path = +outstr2-path = +outstr1-format =llh # (0:llh,1:xyz,2:enu,3:nmea) +outstr2-format =llh # (0:llh,1:xyz,2:enu,3:nmea) +logstr1-type =off # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,6:ntripsvr) +logstr2-type =off # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,6:ntripsvr) +logstr3-type =off # (0:off,1:serial,2:file,3:tcpsvr,4:tcpcli,6:ntripsvr) +logstr1-path = +logstr2-path = +logstr3-path = +misc-svrcycle =10 # (ms) +misc-timeout =10000 # (ms) +misc-reconnect =10000 # (ms) +misc-nmeacycle =5000 # (ms) +misc-buffsize =32768 # (bytes) +misc-navmsgsel =all # (0:all,1:rover,2:base,3:corr) +misc-proxyaddr = +misc-fswapmargin =30 # (s) + diff --git a/web_app/server.py b/web_app/server.py new file mode 100755 index 00000000..e50f00ef --- /dev/null +++ b/web_app/server.py @@ -0,0 +1,565 @@ +#!/usr/bin/python + +# This Flask app is a heavily modified version of Reachview +# modified to be used as a front end for GNSS base +# author: Stéphane Péneau +# source: https://github.com/Stefal/rtkbase + +# ReachView code is placed under the GPL license. +# Written by Egor Fedorov (egor.fedorov@emlid.com) +# Copyright (c) 2015, Emlid Limited +# All rights reserved. + +# If you are interested in using ReachView code as a part of a +# closed source project, please contact Emlid Limited (info@emlid.com). + +# This file is part of ReachView. + +# ReachView is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ReachView is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with ReachView. If not, see . + +#from gevent import monkey +#monkey.patch_all() +import eventlet +eventlet.monkey_patch() + +import time +import json +import os +import signal +import sys +import requests + +from threading import Thread +from RTKLIB import RTKLIB +from port import changeBaudrateTo115200 +from reach_tools import reach_tools, provisioner +from ServiceController import ServiceController +from RTKBaseConfigManager import RTKBaseConfigManager + +#print("Installing all required packages") +#provisioner.provision_reach() + +#import reach_bluetooth.bluetoothctl +#import reach_bluetooth.tcp_bridge + +from threading import Thread +from flask_bootstrap import Bootstrap +from flask import Flask, render_template, session, request, flash, url_for +from flask import send_file, send_from_directory, safe_join, redirect, abort +from flask import g +from flask_wtf import FlaskForm +from wtforms import PasswordField, BooleanField, SubmitField +from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin +from wtforms.validators import ValidationError, DataRequired, EqualTo +from flask_socketio import SocketIO, emit, disconnect +from subprocess import check_output + +from werkzeug.security import generate_password_hash +from werkzeug.security import check_password_hash +from werkzeug.urls import url_parse + +app = Flask(__name__) +app.debug = False +app.config["SECRET_KEY"] = "secret!" +#app.config["UPLOAD_FOLDER"] = os.path.join(os.path.dirname(__file__), "../logs") +app.config["DOWNLOAD_FOLDER"] = os.path.join(os.path.dirname(__file__), "../data") +app.config["LOGIN_DISABLED"] = False + +path_to_rtklib = "/usr/local/bin" + +login=LoginManager(app) +login.login_view = 'login_page' +socketio = SocketIO(app) +bootstrap = Bootstrap(app) + +rtk = RTKLIB(socketio, rtklib_path=path_to_rtklib, log_path=app.config["DOWNLOAD_FOLDER"]) +services_list = [{"service_unit" : "str2str_tcp.service", "name" : "main"}, + {"service_unit" : "str2str_ntrip.service", "name" : "ntrip"}, + {"service_unit" : "str2str_rtcm_svr.service", "name" : "rtcm_svr"}, + {"service_unit" : "str2str_file.service", "name" : "file"}, + ] + + +#Delay before rtkrcv will stop if no user is on status.html page +rtkcv_standby_delay = 600 + +#Get settings from settings.conf.default and settings.conf +rtkbaseconfig = RTKBaseConfigManager(os.path.join(os.path.dirname(__file__), "../settings.conf.default"), os.path.join(os.path.dirname(__file__), "../settings.conf")) + +class User(UserMixin): + """ Class for user authentification """ + def __init__(self, username): + self.id=username + self.password_hash = rtkbaseconfig.get("general", "web_password_hash") + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + +class LoginForm(FlaskForm): + """ Class for the loginform""" + #username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Please enter the password:', validators=[DataRequired()]) + remember_me = BooleanField('Remember Me') + submit = SubmitField('Sign In') + +def update_password(config_object): + """ + Check in settings.conf if web_password entry contains a value + If yes, this function will generate a new hash for it and + remove the web_password value + :param config_object: a RTKBaseConfigManager instance + """ + new_password = config_object.get("general", "new_web_password") + if new_password != "": + config_object.update_setting("general", "web_password_hash", generate_password_hash(new_password)) + config_object.update_setting("general", "new_web_password", "") + +def manager(): + """ This manager runs inside a thread + It checks how long rtkrcv is running since the last user leaves the + status web page, and stop rtkrcv when sleep_count reaches rtkrcv_standby delay + """ + while True: + if rtk.sleep_count > rtkcv_standby_delay and rtk.state != "inactive": + rtk.stopBase() + rtk.sleep_count = 0 + elif rtk.sleep_count > rtkcv_standby_delay: + print("I'd like to stop rtkrcv (sleep_count = {}), but rtk.state is: {}".format(rtk.sleep_count, rtk.state)) + time.sleep(1) + +@socketio.on("check update", namespace="/test") +def check_update(source_url = None, current_release = None, prerelease=True, emit = True): + """ + Check if a RTKBase update exists + :param source_url: the url where we will try to find an update. It uses the github api. + :param current_release: The current RTKBase release + :param prerelease: True/False Get prerelease or not + :param emit: send the result to the web front end with socketio + :return The new release version inside a dict (release version and url for this release) + """ + new_release = {} + source_url = source_url if source_url is not None else "https://api.github.com/repos/stefal/rtkbase/releases" + current_release = current_release if current_release is not None else rtkbaseconfig.get("general", "version").strip("v") + + try: + response = requests.get(source_url) + response = response.json() + for release in response: + if release.get("prerelease") & prerelease or release.get("prerelease") == False: + latest_release = release["tag_name"].strip("v") + if latest_release > current_release and latest_release <= rtkbaseconfig.get("general", "checkpoint_version"): + new_release = {"new_release" : latest_release, "url" : release.get("tarball_url")} + break + + except Exception as e: + print("Check update error: ", e) + + if emit: + socketio.emit("new release", json.dumps(new_release), namespace="/test") + print + return new_release + +@socketio.on("update rtkbase", namespace="/test") +def update_rtkbase(): + """ + Check if a RTKBase exists, download it and update rtkbase + """ + #Check if an update is available + update_url = check_update(emit=False).get("url") + if update_url is None: + return + + import tarfile + #Download update + update_archive = "/var/tmp/rtkbase_update.tar.gz" + try: + response = requests.get(update_url) + with open(update_archive, "wb") as f: + f.write(response.content) + except Exception as e: + print("Error: Can't download update - ", e) + + #Get the "root" folder in the archive + tar = tarfile.open(update_archive) + for tarinfo in tar: + if tarinfo.isdir(): + primary_folder = tarinfo.name + break + + #Extract archive + tar.extractall("/var/tmp") + + #launch update script + rtk.shutdownBase() + rtkbase_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) + source_path = os.path.join("/var/tmp/", primary_folder) + script_path = os.path.join(source_path, "rtkbase_update.sh") + current_release = rtkbaseconfig.get("general", "version").strip("v") + standard_user = rtkbaseconfig.get("general", "user") + os.execl(script_path, "unused arg0", source_path, rtkbase_path, app.config["DOWNLOAD_FOLDER"].split("/")[-1], current_release, standard_user) + +@app.before_request +def inject_release(): + """ + Insert the RTKBase release number as a global variable for Flask/Jinja + """ + g.version = rtkbaseconfig.get("general", "version") + +@login.user_loader +def load_user(id): + return User(id) + +@app.route('/') +@app.route('/index') +@app.route('/status') +@login_required +def status_page(): + """ + The status web page with the gnss satellites levels and a map + """ + return render_template("status.html") + +@app.route('/settings') +@login_required +def settings_page(): + """ + The settings page where you can manage the various services, the parameters, update, power... + """ + main_settings = rtkbaseconfig.get_main_settings() + ntrip_settings = rtkbaseconfig.get_ntrip_settings() + file_settings = rtkbaseconfig.get_file_settings() + rtcm_svr_settings = rtkbaseconfig.get_rtcm_svr_settings() + + return render_template("settings.html", main_settings = main_settings, + ntrip_settings = ntrip_settings, + file_settings = file_settings, + rtcm_svr_settings = rtcm_svr_settings) + +@app.route('/logs') +@login_required +def logs_page(): + """ + The data web pages where you can download/delete the raw gnss data + """ + return render_template("logs.html") + +@app.route("/logs/download/") +@login_required +def downloadLog(log_name): + """ Route for downloading raw gnss data""" + try: + full_log_path = rtk.logm.log_path + "/" + log_name + return send_file(full_log_path, as_attachment = True) + except FileNotFoundError: + abort(404) + +@app.route('/login', methods=['GET', 'POST']) +def login_page(): + if current_user.is_authenticated: + return redirect(url_for('status_page')) + loginform = LoginForm() + if loginform.validate_on_submit(): + user = User('admin') + password = loginform.password.data + if not user.check_password(password): + return abort(401) + + login_user(user, remember=loginform.remember_me.data) + next_page = request.args.get('next') + if not next_page or url_parse(next_page).netloc != '': + next_page = url_for('status_page') + + return redirect(next_page) + + return render_template('login.html', title='Sign In', form=loginform) + +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('login_page')) + +#### Handle connect/disconnect events #### + +@socketio.on("connect", namespace="/test") +def testConnect(): + print("Browser client connected") + rtk.sendState() + +@socketio.on("disconnect", namespace="/test") +def testDisconnect(): + print("Browser client disconnected") + +#### Log list handling ### + +@socketio.on("get logs list", namespace="/test") +def getAvailableLogs(): + print("DEBUG updating logs") + rtk.logm.updateAvailableLogs() + print("Updated logs list is " + str(rtk.logm.available_logs)) + rtk.socketio.emit("available logs", rtk.logm.available_logs, namespace="/test") + +#### str2str launch/shutdown handling #### + +@socketio.on("launch base", namespace="/test") +def launchBase(): + rtk.launchBase() + +@socketio.on("shutdown base", namespace="/test") +def shutdownBase(): + rtk.shutdownBase() + +#### str2str start/stop handling #### + +@socketio.on("start base", namespace="/test") +def startBase(): + rtk.startBase() + +@socketio.on("stop base", namespace="/test") +def stopBase(): + rtk.stopBase() + +@socketio.on("on graph", namespace="/test") +def continueBase(): + rtk.sleep_count = 0 +#### Free space handler + +@socketio.on("get available space", namespace="/test") +def getAvailableSpace(): + rtk.socketio.emit("available space", reach_tools.getFreeSpace(path_to_gnss_log), namespace="/test") + +#### Delete log button handler #### + +@socketio.on("delete log", namespace="/test") +def deleteLog(json): + rtk.logm.deleteLog(json.get("name")) + # Sending the the new available logs + getAvailableLogs() + +#### Download and convert log handlers #### + +@socketio.on("process log", namespace="/test") +def processLog(json): + log_name = json.get("name") + + print("Got signal to process a log, name = " + str(log_name)) + print("Path to log == " + rtk.logm.log_path + "/" + str(log_name)) + + raw_log_path = rtk.logm.log_path + "/" + log_name + rtk.processLogPackage(raw_log_path) + +@socketio.on("cancel log conversion", namespace="/test") +def cancelLogConversion(json): + log_name = json.get("name") + raw_log_path = rtk.logm.log_path + "/" + log_name + rtk.cancelLogConversion(raw_log_path) + +#### RINEX versioning #### + +@socketio.on("read RINEX version", namespace="/test") +def readRINEXVersion(): + rinex_version = rtk.logm.getRINEXVersion() + rtk.socketio.emit("current RINEX version", {"version": rinex_version}, namespace="/test") + +@socketio.on("write RINEX version", namespace="/test") +def writeRINEXVersion(json): + rinex_version = json.get("version") + rtk.logm.setRINEXVersion(rinex_version) + +#### Device hardware functions #### + +@socketio.on("reboot device", namespace="/test") +def rebootRtkbase(): + print("Rebooting...") + rtk.shutdown() + #socketio.stop() hang. I disabled it + #socketio.stop() + check_output("reboot") + +@socketio.on("shutdown device", namespace="/test") +def shutdownRtkbase(): + print("Shutdown...") + rtk.shutdown() + #socketio.stop() hang. I disabled it + #socketio.stop() + check_output(["shutdown", "now"]) + +@socketio.on("turn off wi-fi", namespace="/test") +def turnOffWiFi(): + print("Turning off wi-fi") +# check_output("rfkill block wlan", shell = True) + +#### Systemd Services functions #### + +def load_units(services): + """ + load unit service before getting status + :param services: A list of systemd services (dict) containing a service_unit key:value + :return The dict list updated with the pystemd ServiceController object + + example: + services = [{"service_unit" : "str2str_tcp.service"}] + return will be [{"service_unit" : "str2str_tcp.service", "unit" : a pystemd object}] + + """ + for service in services: + service["unit"] = ServiceController(service["service_unit"]) + return services + +def update_std_user(services): + """ + check which user run str2str_file service and update settings.conf + :param services: A list of systemd services (dict) containing a service_unit key:value + """ + service = next(x for x in services_list if x["name"] == "file") + user = service["unit"].getUser() + rtkbaseconfig.update_setting("general", "user", user) + +def restartServices(restart_services_list): + """ + Restart already running services + This function will refresh all services status, then compare the global services_list and + the restart_services_list to find the services we need to restart. + #TODO I don't really like this global services_list use. + """ + #Update services status + for service in services_list: + service["active"] = service["unit"].isActive() + + #Restart running services + for restart_service in restart_services_list: + for service in services_list: + if service["name"] == restart_service and service["active"] is True: + print("Restarting service: ", service["name"]) + service["unit"].restart() + + #refresh service status + getServicesStatus() + +@socketio.on("get services status", namespace="/test") +def getServicesStatus(): + """ + Get the status of services listed in services_list + (services_list is global) + """ + + print("Getting services status") + + for service in services_list: + service["active"] = service["unit"].isActive() + + services_status = [] + for service in services_list: + services_status.append({key:service[key] for key in service if key != 'unit'}) + + print(services_status) + socketio.emit("services status", json.dumps(services_status), namespace="/test") + return services_status + +@socketio.on("services switch", namespace="/test") +def switchService(json): + """ + Start or stop some systemd services + As a service could need some time to start or stop, there is a 5 seconds sleep + before refreshing the status. + param: json: A json var from the web front end containing one or more service + name with their new status. + """ + print("Received service to switch", json) + try: + for service in services_list: + if json["name"] == service["name"] and json["active"] == True: + print("Trying to start service {}".format(service["name"])) + service["unit"].start() + elif json["name"] == service["name"] and json["active"] == False: + print("Trying to stop service {}".format(service["name"])) + service["unit"].stop() + + except Exception as e: + print(e) + finally: + time.sleep(5) + getServicesStatus() + +@socketio.on("form data", namespace="/test") +def update_settings(json): + """ + Get the form data from the web front end, and save theses values to settings.conf + Then restart the services which have a dependency with these parameters. + param json: A json variable containing the source fom and the new paramaters + """ + print("received settings form", json) + source_section = json.pop().get("source_form") + print("section: ", source_section) + if source_section == "change_password": + if json[0].get("value") == json[1].get("value"): + rtkbaseconfig.update_setting("general", "new_web_password", json[0].get("value")) + update_password(rtkbaseconfig) + socketio.emit("password updated", namespace="/test") + + else: + print("ERREUR, MAUVAIS PASS") + else: + for form_input in json: + print("name: ", form_input.get("name")) + print("value: ", form_input.get("value")) + rtkbaseconfig.update_setting(source_section, form_input.get("name"), form_input.get("value"), write_file=False) + rtkbaseconfig.write_file() + + #Restart service if needed + if source_section == "main": + restartServices(("main", "ntrip", "rtcm_svr", "file")) + elif source_section == "ntrip": + restartServices(("ntrip",)) + elif source_section == "rtcm_svr": + restartServices(("rtcm_svr",)) + elif source_section == "local_storage": + restartServices(("file",)) + +if __name__ == "__main__": + try: + #check if a new password is defined in settings.conf + update_password(rtkbaseconfig) + #check if authentification is required + if not rtkbaseconfig.get_web_authentification(): + app.config["LOGIN_DISABLED"] = True + #get data path + app.config["DOWNLOAD_FOLDER"] = rtkbaseconfig.get("local_storage", "datadir") + #load services status managed with systemd + services_list = load_units(services_list) + #Update standard user in settings.conf + update_std_user(services_list) + #Start a "manager" thread + manager_thread = Thread(target=manager, daemon=True) + manager_thread.start() + + app.secret_key = rtkbaseconfig.get_secret_key() + socketio.run(app, host = "0.0.0.0", port = 80) + + except KeyboardInterrupt: + print("Server interrupted by user!!") + + # clean up broadcast and blink threads + rtk.server_not_interrupted = False +# rtk.led.blinker_not_interrupted = False + rtk.waiting_for_single = False + + if rtk.coordinate_thread is not None: + rtk.coordinate_thread.join() + + if rtk.satellite_thread is not None: + rtk.satellite_thread.join() + +# if rtk.led.blinker_thread is not None: +# rtk.led.blinker_thread.join() + diff --git a/web_app/static/graph.js b/web_app/static/graph.js new file mode 100644 index 00000000..e9bf325f --- /dev/null +++ b/web_app/static/graph.js @@ -0,0 +1,409 @@ +function Chart() { + +// orig set for 20 satellites + +// this.chartdata = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.chartdata1 = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; + +// set for 21 +// this.chartdata = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.chartdata1 = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; +// this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; +// this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; + + + + this.chartdata = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'},{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}]; + this.chartdata1 = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'},{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}]; + this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; + + this.height = parseInt(55*5); + this.roverBars = ''; + this.baseBars = ''; + this.labels = ''; + this.svg = ''; + this.vAxis = ''; + this.verticalGuide = ''; + this.xScale = ''; + this.hAxis = ''; + this.horizontalGuide = ''; + + this.create = function(){ + + var grid_style = { + borderRight: "1x solid #ddd", + borderTop: "1px solid #ddd", + textAlign: "left", + borderCollapse: 'collapse', + fontSize: '15px' + }; + + $("#status_block").css({borderRight: '1px solid #ddd'}); + $("#mode_block").css({borderRight: '1px solid #ddd'}); + $("#lat_block").css(grid_style); + $("#lon_block").css(grid_style); + $("#height_block").css(grid_style); + $('.ui-grid-b .ui-bar').css({borderBottom: '1px solid #ddd',borderRight: '1px solid #ddd'}); + + // Default values for the info boxes + + $("#mode_value").text("no link"); + $("#status_value").text("no link"); + $("#lon_value").text("0"); + $("#lat_value").text("0"); + $("#height_value").html("0"); + + var height = 55*5; + var margin = {top: 30, right: 10, bottom: 30, left: 40}; + // the size of the overall svg element + var width = $("#bar-chart").width() - margin.left - margin.right, + barWidth = width*0.03, + barOffset = width*0.01; + + this.svg = d3.select('#bar-chart').append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .style('background', 'white'); + + var yScale = d3.scale.linear() + .domain([0, 55]) + .range([0, height]) + + this.xScale = d3.scale.ordinal() + .rangeBands([0, width]) + + var verticalGuideScale = d3.scale.linear() + .domain([0, 55]) + .range([height, 0]) + + this.vAxis = d3.svg.axis() + .scale(verticalGuideScale) + .orient('left') + .ticks(10) + .tickSize(-width, 0, 0) + + this.verticalGuide = d3.select('svg').append('g') + this.vAxis(this.verticalGuide) + this.verticalGuide.attr('transform', 'translate(' + 30 + ', ' + margin.top + ')') + this.verticalGuide.selectAll('path') + .style({fill: 'none', stroke: "black"}) + this.verticalGuide.selectAll('line') + .style({stroke: "rgba(0,0,0,0.2)"}) + + + this.hAxis = d3.svg.axis() + .scale(this.xScale) + .orient('bottom') + + this.horizontalGuide = d3.select('svg').append('g') + this.hAxis(this.horizontalGuide) + this.horizontalGuide.attr('transform', 'translate(' + 30 + ', ' + (height + margin.top) + ')') + this.horizontalGuide.selectAll('path') + .style({fill: 'none', stroke: "black"}) + this.horizontalGuide.selectAll('line') + .style({stroke: "black"}); + + this.roverBars = this.svg.append('g') + .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')') + .selectAll('rect').data(this.chartdata) + .enter().append('rect') + .style("fill", function(data) { return data.color; }) + .style({stroke: "black"}) + .attr('width', barWidth/2) + .attr('height', function (data) { + return 5*data.value; + }) + .attr('x', function (data, i) { + return i * (barWidth + barOffset); + }) + .attr('y', function (data) { + return (55*5 - 5*data.value); + }); + + this.baseBars = this.svg.append('g') + .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')') + .selectAll('rect').data(this.chartdata1) + .enter().append('rect') + .style("fill", function(data) { return data.color; }) + .style({stroke: "black"}) + .attr('width', barWidth/2) + .attr('height', function (data) { + return 5*data.value; + }) + .attr('x', function (data, i) { + return i * (barWidth + barOffset) + barWidth/2; + }) + .attr('y', function (data) { + return (55*5 - 5*data.value); + }); + + this.labels = this.svg.append("g") + .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')') + .attr("class", "labels") + .selectAll("text") + .data(this.labeldata) + .enter() + .append("text") + .attr("dx", function(d, i) { + return (i * (barWidth + barOffset)) + barWidth/2-14 + }) + .attr("dy", height + 20) + .text(function(d) { + return d; + }) + .style("font-size","13px"); + } + + this.resize = function(){ + + var margin = {top: 30, right: 10, bottom: 30, left: 40}; + var width = $("#bar-chart").width() - margin.left - margin.right; + + var barWidth = width*0.03; + var barOffset = width*0.01; + this.svg.attr('width', width + margin.left + margin.right) + + this.roverBars.attr("width", barWidth/2) + .attr('x', function (data, i) { + return i * (barWidth + barOffset); + }) + this.baseBars.attr("width", barWidth/2) + .attr('x', function (data, i) { + return i * (barWidth + barOffset) + barWidth/2; + }) + this.labels.attr("dx", function(d, i) { + return (i * (barWidth + barOffset)) + barWidth/2-14; + }) + this.vAxis.tickSize(-width, 0, 0) + this.vAxis(this.verticalGuide) + + this.xScale.rangeBands([0, width]) + this.hAxis.scale(this.xScale) + this.hAxis(this.horizontalGuide) + } + + this.roverUpdate = function(msg){ + + // msg object contains satellite data for rover in {"name0": "level0", "name1": "level1"} format + + // we want to display the top ? results + var number_of_satellites = 24; + + // graph has a list of datasets. rover sat values are in the first one + var rover_dataset_number = 1; + + // first, we convert the msg object into a list of satellites to make it sortable + + var new_sat_values = []; + + for (var k in msg) { + new_sat_values.push({sat:k, level:msg[k]}); + } + + // sort the sat levels by ascension +// new_sat_values.sort(function(a, b) { +// var diff = a.level - b.level; +// if (Math.abs(diff) < 3) { +// diff = 0; +// } +// return diff +// }); + + // next step is to cycle through top 10 values if they exist + // and extract info about them: level, name, and define their color depending on the level + + var new_sat_values_length = new_sat_values.length; + var new_sat_levels = []; + var new_sat_labels = []; + var new_sat_fillcolors = []; + + for(var i = new_sat_values_length - number_of_satellites; i < new_sat_values_length; i++) { + // check if we actually have enough satellites to plot: + if (i < 0) { + // we have less than number_of_satellites to plot + // so we fill the first bars of the graph with zeroes and stuff + new_sat_levels.push(0); + new_sat_labels.push(""); + new_sat_fillcolors.push("rgba(0, 0, 0, 0.9)"); + } else { + // we have gotten to useful data!! let's add it to the the array too + + // for some reason I sometimes get undefined here. So plot zero just to be safe + var current_level = parseInt(new_sat_values[i].level) || 0; + var current_fillcolor; + + // determine the fill color depending on the sat level + switch(true) { + case (current_level < 20): + current_fillcolor = "#FF766C"; // Red + break; + case (current_level >= 20 && current_level <= 33): + current_fillcolor = "#FFEA5B"; // Yellow + break; + case (current_level >= 33): + current_fillcolor = "#44D62C"; // Green + break; + } + + new_sat_levels.push(current_level); + new_sat_labels.push(new_sat_values[i].sat); + new_sat_fillcolors.push(current_fillcolor); + } + } + + for (var i = 0; i < new_sat_levels.length; i++) { + this.chartdata[i]['value'] = new_sat_levels[i]; + this.chartdata[i]['color'] = new_sat_fillcolors[i]; + this.labeldata[i] = new_sat_labels[i]; + }; + + this.roverBars.data(this.chartdata) + .transition() + .attr('height', function (data) { + return 5*data.value; + }) + .attr('y', function (data) { + return (55*5 - 5*data.value); + }) + .style("fill", function(data) { return data.color; }) + .duration(300); + + this.labels.data(this.labeldata) + .text(function(d) { + return d; + }); + } + + + this.baseUpdate = function(msg){ + var base_dataset_number = 0; + var current_level = 0; + var current_fillcolor; + var new_sat_levels = []; + // var new_sat_labels = []; + var new_sat_fillcolors = []; + + // cycle through the graphs's labels and extract base levels for them + this.labeldata.forEach(function(label, label_index) { + if (label in msg) { + // get the sat level as an integer + current_level = parseInt(msg[label]); + + new_sat_levels.push(current_level); + new_sat_fillcolors.push("#d9d9d9"); + + } else { + // if we don't the same satellite in the base + new_sat_levels.push(0); + new_sat_fillcolors.push("#d9d9d9"); + } + + }); + for (var i = 0; i < new_sat_levels.length; i++) { + this.chartdata1[i]['value'] = new_sat_levels[i]; + this.chartdata1[i]['color'] = new_sat_fillcolors[i]; + }; + + if(JSON.stringify(msg) == JSON.stringify(lastBaseMsg)){ + numOfRepetition++; + } + else{ + lastBaseMsg = msg; + numOfRepetition = 0; + } + + if(numOfRepetition >= 5) + this.chartdata1 = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; + + this.baseBars.data(this.chartdata1) + .transition() + .attr('height', function (data) { + return 5*data.value; + }) + .attr('y', function (data) { + return (55*5 - 5*data.value); + }) + .style("fill", function(data) { return data.color; }) + .duration(300); + } + + this.cleanStatus = function(mode, status) { +// this.chartdata = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.chartdata1 = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; + +// this.chartdata = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.chartdata1 = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}]; +// this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; + + + this.chartdata = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'},{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}]; + this.chartdata1 = [{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'},{'value':'', 'color':'rgba(255,0,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(0,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}, {'value':'', 'color':'rgba(255,255,0,0.5)'}]; +// this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; + this.labeldata = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; + + + console.log("Got signal to clean the graph") + + mode = typeof mode !== "undefined" ? mode : "rtklib stopped"; + status = typeof status !== "undefined" ? status : "-"; + + var empty_string_list = []; + for (var i = 0; i < 10; i++) { + empty_string_list[i] = ""; + } + + this.roverBars.data(this.chartdata) + .transition() + .attr('height', function (data) { + return 5*data.value; + }) + .attr('y', function (data) { + return (55*5 - 5*data.value); + }) + .style("fill", function(data) { return data.color; }) + .duration(300); + + this.baseBars.data(this.chartdata) + .transition() + .attr('height', function (data) { + return 5*data.value; + }) + .attr('y', function (data) { + return (55*5 - 5*data.value); + }) + .style("fill", function(data) { return data.color; }) + .duration(300); + + this.labels.data(this.labeldata) + .text(function(d) { + return d; + }); + + var msg = { + "lat" : "0", + "lon" : "0", + "height": "0", + "solution status": status, + "positioning mode": mode + }; + + updateCoordinateGrid(msg) + } + +} + +function updateCoordinateGrid(msg) { + // coordinates + var coordinates = (typeof(msg['pos llh single (deg,m) rover']) == 'undefined') ? '000' : msg['pos llh single (deg,m) rover'].split(','); + + var lat_value = coordinates[0].substring(0, 11) + Array(11 - coordinates[0].substring(0, 11).length + 1).join(" "); + var lon_value = coordinates[1].substring(0, 11) + Array(11 - coordinates[1].substring(0, 11).length + 1).join(" "); + var height_value = coordinates[2].substring(0, 11) + Array(11 - coordinates[2].substring(0, 11).length + 1 + 2).join(" "); + + $("#lat_value").html("" + lat_value + " °" + ""); + $("#lon_value").html("" + lon_value + " °" + ""); + $("#height_value").html("" + height_value + "m" + ""); +} diff --git a/web_app/static/images/bar.svg b/web_app/static/images/bar.svg new file mode 100644 index 00000000..7dcc04b7 --- /dev/null +++ b/web_app/static/images/bar.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web_app/static/images/download.svg b/web_app/static/images/download.svg new file mode 100644 index 00000000..5a9710cd --- /dev/null +++ b/web_app/static/images/download.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web_app/static/images/files.svg b/web_app/static/images/files.svg new file mode 100644 index 00000000..988a2572 --- /dev/null +++ b/web_app/static/images/files.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web_app/static/images/gear.svg b/web_app/static/images/gear.svg new file mode 100644 index 00000000..30dab5f4 --- /dev/null +++ b/web_app/static/images/gear.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web_app/static/images/pencil.svg b/web_app/static/images/pencil.svg new file mode 100644 index 00000000..cba81d42 --- /dev/null +++ b/web_app/static/images/pencil.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web_app/static/images/trash.svg b/web_app/static/images/trash.svg new file mode 100644 index 00000000..148707ef --- /dev/null +++ b/web_app/static/images/trash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web_app/static/images/trash_fill.svg b/web_app/static/images/trash_fill.svg new file mode 100644 index 00000000..d59714f6 --- /dev/null +++ b/web_app/static/images/trash_fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web_app/static/lib/bootstrap-table.min.css b/web_app/static/lib/bootstrap-table.min.css new file mode 100644 index 00000000..72a8f74f --- /dev/null +++ b/web_app/static/lib/bootstrap-table.min.css @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.16.0 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==)}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII=)}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;text-align:left!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:none;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{font-size:2rem;margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination a{padding:6px 12px;line-height:1.428571429}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:'\2B05'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:'\27A1'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}} \ No newline at end of file diff --git a/web_app/static/lib/bootstrap-table.min.js b/web_app/static/lib/bootstrap-table.min.js new file mode 100644 index 00000000..e37e1a7e --- /dev/null +++ b/web_app/static/lib/bootstrap-table.min.js @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.16.0 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t=t||self).BootstrapTable=e(t.jQuery)}(this,(function(t){"use strict";t=t&&t.hasOwnProperty("default")?t.default:t;var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function i(t,e){return t(e={exports:{}},e.exports),e.exports}var n=function(t){return t&&t.Math==Math&&t},o=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof e&&e)||Function("return this")(),r=function(t){try{return!!t()}catch(t){return!0}},a=!r((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})),s={}.propertyIsEnumerable,l=Object.getOwnPropertyDescriptor,c={f:l&&!s.call({1:2},1)?function(t){var e=l(this,t);return!!e&&e.enumerable}:s},h=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},u={}.toString,d=function(t){return u.call(t).slice(8,-1)},f="".split,p=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==d(t)?f.call(t,""):Object(t)}:Object,g=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},v=function(t){return p(g(t))},b=function(t){return"object"==typeof t?null!==t:"function"==typeof t},m=function(t,e){if(!b(t))return t;var i,n;if(e&&"function"==typeof(i=t.toString)&&!b(n=i.call(t)))return n;if("function"==typeof(i=t.valueOf)&&!b(n=i.call(t)))return n;if(!e&&"function"==typeof(i=t.toString)&&!b(n=i.call(t)))return n;throw TypeError("Can't convert object to primitive value")},y={}.hasOwnProperty,w=function(t,e){return y.call(t,e)},S=o.document,x=b(S)&&b(S.createElement),k=function(t){return x?S.createElement(t):{}},O=!a&&!r((function(){return 7!=Object.defineProperty(k("div"),"a",{get:function(){return 7}}).a})),C=Object.getOwnPropertyDescriptor,T={f:a?C:function(t,e){if(t=v(t),e=m(e,!0),O)try{return C(t,e)}catch(t){}if(w(t,e))return h(!c.f.call(t,e),t[e])}},P=function(t){if(!b(t))throw TypeError(String(t)+" is not an object");return t},$=Object.defineProperty,I={f:a?$:function(t,e,i){if(P(t),e=m(e,!0),P(i),O)try{return $(t,e,i)}catch(t){}if("get"in i||"set"in i)throw TypeError("Accessors not supported");return"value"in i&&(t[e]=i.value),t}},A=a?function(t,e,i){return I.f(t,e,h(1,i))}:function(t,e,i){return t[e]=i,t},E=function(t,e){try{A(o,t,e)}catch(i){o[t]=e}return e},R=o["__core-js_shared__"]||E("__core-js_shared__",{}),j=Function.toString;"function"!=typeof R.inspectSource&&(R.inspectSource=function(t){return j.call(t)});var N,F,_,B=R.inspectSource,V=o.WeakMap,L="function"==typeof V&&/native code/.test(B(V)),D=i((function(t){(t.exports=function(t,e){return R[t]||(R[t]=void 0!==e?e:{})})("versions",[]).push({version:"3.6.0",mode:"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})})),H=0,M=Math.random(),U=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++H+M).toString(36)},z=D("keys"),q=function(t){return z[t]||(z[t]=U(t))},W={},G=o.WeakMap;if(L){var K=new G,J=K.get,Y=K.has,X=K.set;N=function(t,e){return X.call(K,t,e),e},F=function(t){return J.call(K,t)||{}},_=function(t){return Y.call(K,t)}}else{var Q=q("state");W[Q]=!0,N=function(t,e){return A(t,Q,e),e},F=function(t){return w(t,Q)?t[Q]:{}},_=function(t){return w(t,Q)}}var Z,tt={set:N,get:F,has:_,enforce:function(t){return _(t)?F(t):N(t,{})},getterFor:function(t){return function(e){var i;if(!b(e)||(i=F(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return i}}},et=i((function(t){var e=tt.get,i=tt.enforce,n=String(String).split("String");(t.exports=function(t,e,r,a){var s=!!a&&!!a.unsafe,l=!!a&&!!a.enumerable,c=!!a&&!!a.noTargetGet;"function"==typeof r&&("string"!=typeof e||w(r,"name")||A(r,"name",e),i(r).source=n.join("string"==typeof e?e:"")),t!==o?(s?!c&&t[e]&&(l=!0):delete t[e],l?t[e]=r:A(t,e,r)):l?t[e]=r:E(e,r)})(Function.prototype,"toString",(function(){return"function"==typeof this&&e(this).source||B(this)}))})),it=o,nt=function(t){return"function"==typeof t?t:void 0},ot=function(t,e){return arguments.length<2?nt(it[t])||nt(o[t]):it[t]&&it[t][e]||o[t]&&o[t][e]},rt=Math.ceil,at=Math.floor,st=function(t){return isNaN(t=+t)?0:(t>0?at:rt)(t)},lt=Math.min,ct=function(t){return t>0?lt(st(t),9007199254740991):0},ht=Math.max,ut=Math.min,dt=function(t,e){var i=st(t);return i<0?ht(i+e,0):ut(i,e)},ft=function(t){return function(e,i,n){var o,r=v(e),a=ct(r.length),s=dt(n,a);if(t&&i!=i){for(;a>s;)if((o=r[s++])!=o)return!0}else for(;a>s;s++)if((t||s in r)&&r[s]===i)return t||s||0;return!t&&-1}},pt={includes:ft(!0),indexOf:ft(!1)},gt=pt.indexOf,vt=function(t,e){var i,n=v(t),o=0,r=[];for(i in n)!w(W,i)&&w(n,i)&&r.push(i);for(;e.length>o;)w(n,i=e[o++])&&(~gt(r,i)||r.push(i));return r},bt=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],mt=bt.concat("length","prototype"),yt={f:Object.getOwnPropertyNames||function(t){return vt(t,mt)}},wt={f:Object.getOwnPropertySymbols},St=ot("Reflect","ownKeys")||function(t){var e=yt.f(P(t)),i=wt.f;return i?e.concat(i(t)):e},xt=function(t,e){for(var i=St(e),n=I.f,o=T.f,r=0;rr;)I.f(t,i=n[r++],e[i]);return t},Vt=ot("document","documentElement"),Lt=q("IE_PROTO"),Dt=function(){},Ht=function(t){return" + + + + {% endblock %} + + diff --git a/web_app/templates/login.html b/web_app/templates/login.html new file mode 100644 index 00000000..a1358301 --- /dev/null +++ b/web_app/templates/login.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% from 'bootstrap/form.html' import render_form %} + {{ render_form(form) }} +
+{% endblock %} \ No newline at end of file diff --git a/web_app/templates/logs.html b/web_app/templates/logs.html new file mode 100644 index 00000000..d0f56ac8 --- /dev/null +++ b/web_app/templates/logs.html @@ -0,0 +1,84 @@ +{% extends 'base.html' %} + +{% block styles %} + +{{super()}} +{% endblock %} + +{% block content %} +
+ + + + + + + + + + + +
File nametypesize (MB)actions
+
+ + + + + + +{% endblock %} + +{% block scripts %} +{{super()}} + + + +{% endblock %} \ No newline at end of file diff --git a/web_app/templates/settings.html b/web_app/templates/settings.html new file mode 100644 index 00000000..5a71b9ec --- /dev/null +++ b/web_app/templates/settings.html @@ -0,0 +1,359 @@ +{% extends 'base.html' %} + +{% block styles %} +{{super()}} + + +{% endblock %} + +{% block content %} +
+

Services:

+ +
+
+ + + +
+
+
+ +
+ +
+ +
Antenna's coordinates: latitude longitude elevation
+
+
+
+ + +
+ +
Gnss receiver com port (without /dev/)
+
+
+
+ + +
+ +
Com port settings: baudrate:data bits:parity:stop bits:flow control
+
+
+
+ + +
+ +
Gnss receiver brand and model
+ +
+
+
+ + +
+ +
Gnss receiver format (ubx,sqt,tersus,...)
+ +
+ +
+ +
+ + +
+ +
Local tcp port
+ +
+ +
+
+ +
+
+ + +
+
+ + + +
+
+
+
+ +
+ +
Caster url address
+
+
+
+ +
+ +
Caster port
+
+
+
+ +
+ +
Caster password
+
+
+
+ +
+ +
Mount name
+
+
+
+ +
+ +
Rtcm messages list
+
+
+ + +
+ +
+
+ + +
+
+ + + +
+
+
+
+ +
+ +
Rtcm server port
+
+
+
+ +
+ +
Rtcm server messages list
+
+
+ +
+ +
+
+ + +
+
+ + + +
+
+
+
+ +
+ +
Path to data directory
+
+
+
+ +
+ +
File name construction (should start with %Y-%m-%d_%h-%M-%S)
+
+
+
+ +
+ +
File rotation time (in hour)
+
+
+
+ +
+ +
File rotation time (in seconds)
+
+
+
+ +
+ +
Archive duration before deletion (in days)
+
+
+
+ +
+
+
+
+

System Settings:

+
+
+ Rtkbase {{ g.version }} +
+
+ +
+
+ +
+
+ Change Password: +
+
+
+ +
+ +
+ +
New password
+
+
+
+ +
+ +
Confirm password
+
+
+
+ +
+
+
+
+ + +
+
+ Power: +
+
+ + +
+
+ +
+ + + + + + + + + + + + + +{% endblock %} + + +{% block scripts %} +{{super()}} + + +{% endblock %} \ No newline at end of file diff --git a/web_app/templates/status.html b/web_app/templates/status.html new file mode 100644 index 00000000..b15f224d --- /dev/null +++ b/web_app/templates/status.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block content %} + +
+
+ +
+
Latitude:
+
Longitude:
+
Height:
+
+ +
+ +
+ +
+{% endblock %} + +{% block scripts %} +{{super()}} + + + + + +{% endblock %} \ No newline at end of file