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:
+|
|
|
|
-+ 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:
+
-+ 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.
+
-+ 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.
+
+
+
+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 %}
+