diff --git a/luci-app-gpoint-main/Images/logo.png b/luci-app-gpoint-main/Images/logo.png new file mode 100644 index 000000000..cd81bc607 Binary files /dev/null and b/luci-app-gpoint-main/Images/logo.png differ diff --git a/luci-app-gpoint-main/Images/overview.png b/luci-app-gpoint-main/Images/overview.png new file mode 100644 index 000000000..4942bf81d Binary files /dev/null and b/luci-app-gpoint-main/Images/overview.png differ diff --git a/luci-app-gpoint-main/Images/overview_wait.png b/luci-app-gpoint-main/Images/overview_wait.png new file mode 100644 index 000000000..ebe30a42d Binary files /dev/null and b/luci-app-gpoint-main/Images/overview_wait.png differ diff --git a/luci-app-gpoint-main/Images/settings.png b/luci-app-gpoint-main/Images/settings.png new file mode 100644 index 000000000..320183474 Binary files /dev/null and b/luci-app-gpoint-main/Images/settings.png differ diff --git a/luci-app-gpoint-main/LICENSE b/luci-app-gpoint-main/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-app-gpoint-main/LICENSE @@ -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 +. diff --git a/luci-app-gpoint-main/Makefile b/luci-app-gpoint-main/Makefile new file mode 100644 index 000000000..6accd6e1e --- /dev/null +++ b/luci-app-gpoint-main/Makefile @@ -0,0 +1,14 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=GNSS Information dashboard for 3G/LTE dongle +LUCI_DEPENDS:=+lua +curl +lua-rs232 +luasocket +iwinfo +libiwinfo-lua +lua-bit32 +PKG_LICENSE:=GPLv3 +PKG_VERSION:=1.6.0 + +define Package/luci-app-gpoint/conffiles + /etc/config/gpoint +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-gpoint-main/README.md b/luci-app-gpoint-main/README.md new file mode 100644 index 000000000..2c2fc338f --- /dev/null +++ b/luci-app-gpoint-main/README.md @@ -0,0 +1,45 @@ +

+ Gpoint +
Global Navigation Satellite System for OpenWrt LuCi
+

+

Gpoint was created in order to use full set of functions of mobile modules installed in OpenWRT router. +Manufacturers of GSM/3G/LTE modems often lay down GNSS function, so why not use it? +It doesn't matter if you use a router in transport or it is installed in your terminal, you can always find out its location!

+
+ Screenshots + overview_wait + overview + overview +
+ +## Features +- Support: GPS, GLONASS (works with "NMEA 0183" standard protocol) +- GeoHash (reduces drift of GPS\GLONASS coordinate readings in parking) +- [Kalman filter](https://github.com/lacker/ikalman) (Implementation of Kalman filter for geo (gps) tracks. This is a Lua port of original C code) +- Yandex Locator [API](https://yandex.ru/dev/locator/) (Determines location by nearest Wi-Fi access points) +- Server side (sends GNSS data to a remote server) +- Support [OpenLayers](https://openlayers.org/) maps in UI, and much more! + +## Supported devices +- Dell DW5821e/DW5829e +- Quectel EC25/EP06/EM12/RM500Q +- Sierra EM7455/EM7565 +- Simcom SIM7600E-H +- U-Blox VK-172 GPS/GLONASS module (u-blox 7 GNSS modules) + + +## Supported GNSS protocols +- [OsmAnd](https://www.traccar.org/osmand/) +- [Wialon IPS](https://gurtam.com/ru/gps-hardware/soft/wialon-ips) + +## Install +- Upload ipk file to tmp folder +- cd /tmp +- opkg update +- opkg install luci-app-gpoint_1.7.0_all.ipk + +## Uninstall +- opkg remove luci-app-gpoint + +## License +Gpoint like OpenWRT is released under the GPL v3.0 License - see detailed [LICENSE](https://github.com/Kodo-kakaku/luci-app-gpoint/blob/main/LICENSE). diff --git a/luci-app-gpoint-main/htdocs/luci-static/resources/icons/gpoint_icons/marker.png b/luci-app-gpoint-main/htdocs/luci-static/resources/icons/gpoint_icons/marker.png new file mode 100644 index 000000000..950edf246 Binary files /dev/null and b/luci-app-gpoint-main/htdocs/luci-static/resources/icons/gpoint_icons/marker.png differ diff --git a/luci-app-gpoint-main/htdocs/luci-static/resources/icons/gpoint_icons/spiner.gif b/luci-app-gpoint-main/htdocs/luci-static/resources/icons/gpoint_icons/spiner.gif new file mode 100644 index 000000000..c78a08281 Binary files /dev/null and b/luci-app-gpoint-main/htdocs/luci-static/resources/icons/gpoint_icons/spiner.gif differ diff --git a/luci-app-gpoint-main/luasrc/controller/gpoint/gpoint.lua b/luci-app-gpoint-main/luasrc/controller/gpoint/gpoint.lua new file mode 100644 index 000000000..a16f77adf --- /dev/null +++ b/luci-app-gpoint-main/luasrc/controller/gpoint/gpoint.lua @@ -0,0 +1,86 @@ +------------------------------------------------------------- +-- luci-app-gpoint. Gnss information dashboard for 3G/LTE dongle. +------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local ubus = require("ubus") +local uci = require("luci.model.uci") +local json = require("luci.jsonc") + +module("luci.controller.gpoint.gpoint", package.seeall) + +function index() + entry({"admin", "services", "gpoint"}, alias ("admin","services", "gpoint", "map"), translate("GPoint"), 10).acl_depends={"unauthenticated"} + entry({"admin", "services", "gpoint", "map"}, template("gpoint/overview"), translate("Overview"), 51).acl_depends={"unauthenticated"} + entry({"admin", "services", "gpoint", "settings"}, cbi("gpoint/gpoint"), translate("Settings"), 52).acl_depends={"unauthenticated"} + entry({"admin", "services", "gpoint", "action"}, call("gpoint_action"), nil).leaf = true + entry({"admin", "services", "gpoint", "geopoint"}, call("get_geopoint"), nil).leaf = true + entry({"admin", "services", "gpoint", "blackbox"}, call("get_blackbox"), nil).leaf = true +end + +local serviceIsStop = { + warning={ + app={true,"Service stop"}, + server={true,"Service stop"}, + filter={true,"Service stop"}, + locator={true,"Service stop"}, + kalman={true,"Service stop"} + } +} + +local serviceUbusFailed = { + warning={ + app={true,"Ubus Failed"}, + server={true,"Loading..."}, + filter={true,"Loading..."}, + locator={true,"Loading..."}, + kalman ={true, "Loading..."} + } +} + +-- Overview JSON request +function get_geopoint() + local sessionId = uci:get("gpoint", "service_settings", "sessionid") + luci.http.prepare_content("application/json") + local data + if sessionId == "stop" then + data = json.stringify(serviceIsStop) + else + local conn = ubus.connect() + if conn then + local resp = conn:call("session", "list", {ubus_rpc_session = sessionId}) + data = json.stringify(resp.data) + conn:close() + else + data = json.stringify(serviceUbusFailed) + end + + end + luci.http.write(data) +end + +-- BlackBox JSON request +function get_blackbox() + local data = luci.sys.exec("cat /usr/share/gpoint/tmp/blackbox.json") + luci.http.prepare_content("application/json") + luci.http.write(data) +end + +-- Settings init.d service +function gpoint_action(name) + local packageName = "gpoint" + if name == "start" then + luci.sys.init.start(packageName) + elseif name == "action" then + luci.util.exec("/etc/init.d/" .. packageName .. " reload") + elseif name == "stop" then + luci.sys.init.stop(packageName) + elseif name == "enable" then + luci.sys.init.enable(packageName) + elseif name == "disable" then + luci.sys.init.disable(packageName) + end + luci.http.prepare_content("text/plain") + luci.http.write("0") +end diff --git a/luci-app-gpoint-main/luasrc/model/cbi/gpoint/gpoint.lua b/luci-app-gpoint-main/luasrc/model/cbi/gpoint/gpoint.lua new file mode 100644 index 000000000..c262b8958 --- /dev/null +++ b/luci-app-gpoint-main/luasrc/model/cbi/gpoint/gpoint.lua @@ -0,0 +1,252 @@ +------------------------------------------------------------- +-- luci-app-gpoint. Gnss information dashboard for 3G/LTE dongle. +------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local fs = require("nixio.fs") +local sys = require("luci.sys") +local util = require("luci.util") +local json = require("luci.jsonc") + + +local packageName = "gpoint" +local helperText = "" +local tmpfsStatus, tmpfsStatusCode +local ubusStatus = util.ubus("service", "list", { name = packageName }) +local lsusb = sys.exec("lsusb") +local device_port = fs.glob("/dev/tty[A-Z][A-Z]*") + +local timezone = { + {'Autodetect (experimental)','auto'},{'Etc/GMT', '0' },{ 'Etc/GMT+1', '1' },{ 'Etc/GMT+10', '10'}, + { 'Etc/GMT+11', '11'},{ 'Etc/GMT+12', '12'},{ 'Etc/GMT+2', '2' },{ 'Etc/GMT+3', '3' }, + { 'Etc/GMT+4', '4' },{ 'Etc/GMT+5', '5' },{ 'Etc/GMT+6', '6' },{ 'Etc/GMT+7', '7' }, + { 'Etc/GMT+8', '8' },{ 'Etc/GMT+9', '9' },{ 'Etc/GMT-1', '-1' },{ 'Etc/GMT-10', '-10'}, + { 'Etc/GMT-11', '-11'},{ 'Etc/GMT-12', '-12'},{ 'Etc/GMT-13', '-13'},{ 'Etc/GMT-14', '-14'}, + { 'Etc/GMT-2', '-2' },{ 'Etc/GMT-3', '-3' },{ 'Etc/GMT-4', '-4' },{ 'Etc/GMT-5', '-5' }, + { 'Etc/GMT-6', '-6' },{ 'Etc/GMT-7', '-7' },{ 'Etc/GMT-8', '-8' },{ 'Etc/GMT-9', '-9' } +} + +local modems = { + ["Quectel"] = { + ["2c7c:0306"] = "EP06", + ["2c7c:0512"] = "EM12", + ["2c7c:0125"] = "EC25", + ["2c7c:0800"] = "RM500Q" + }, + ["Sierra"] = { + ["1199:9071"] = "EM7455", + ["1199:9091"] = "EM7565" + }, + ["U-Blox"] = { + ["1546:01a7"] = "VK-172" + }, + ["Simcom"] = { + ["1e0e:9001"] = "SIM7600E-H" + }, + ["Dell"] = { + ["413c:81d7"] = "DW5821e", + ["413c:81e6"] = "DW5829e" + } + +} + +local m = Map("gpoint", translate("")) + +-- Service +local s = m:section(TypedSection, "modem_settings", translate("Service")) +s.anonymous = true +s.addremove = false + +local o = s:option(DummyValue, "_dummy") +o.template = packageName .. "/buttons" + +o = s:option(DummyValue, "_dummy", translate("Service Status:")) +o.template = packageName .. "/service_status" + + +-- Modem +s = m:section(TypedSection, "modem_settings", translate("Modem"), translate("Select the modem(s) to find the location")) +s.anonymous = true +s.addremove = false + +local no_device = true +o = s:option(ListValue, "modem", translate("Modem(s):")) +if lsusb then + for modem_name, modem_data in pairs(modems) do + for id, modem in pairs(modem_data) do + if string.find(lsusb, id) then + no_device = false + o:value(modem_name .. '_' .. modem, modem_name .. ' ' .. modem) + end + end + end +end + +if no_device then + o:value('mnf', translate("-- Modems not found --")) +end + +o = s:option(ListValue, "port", translate("Modem port:"), translate("Select the NMEA port of the device.")) +if no_device then + o:value('pnf', translate("-- disable --")) + o = s:option( DummyValue, "nfound") + function o.cfgvalue(self, section) + local nfound = "
No modem(s) found! Check the modem connections.
\ +
Supported modems: " + for modem_name, modem_data in pairs(modems) do + nfound = nfound .. "
" .. modem_name .. ' ' + for _, modem in pairs(modem_data) do + nfound = nfound .. modem .. ", " + end + nfound = nfound:sub(1, -3) + end + nfound = nfound .. "
" + return translate(nfound) + end + o.rawhtml = true +else + if device_port then + for node in device_port do + o:value(node, node) + end + end +end + +-- Add TimeZone +o = s:option(ListValue, "timezone", translate("Timezone:")) +for _, zone in pairs(timezone) do + o:value(zone[2], zone[1]) +end + + +-- Remote Server +s = m:section(TypedSection, "server_settings", translate("Remote Server"), translate("Configuration of the remote navigation server")) +s.addremove = false +s.anonymous = true + +o = s:option(Flag, "server_enable", translate("Enable server:"), translate("Enabling Remote Server service")) + +o = s:option(ListValue, "proto", translate(" "), translate("Navigation data transmission protocol")) +o.widget = "radio" +o:value("traccar", " Traccar Client") -- Key and value pairs +o:value("wialon", " Wialon IPS") +o.default = "trackcar" + +o = s:option(Value, "server_frequency", translate("Frequency:"), translate("Frequency of sending data to the Remote Server")) +o.placeholder = "In seconds" +o.datatype = "range(5, 600)" + +o = s:option(Value, "server_ip", translate("Address:")) +o.datatype = "host" +o.placeholder = '172.0.0.1' + +o = s:option(Value, "server_port", translate("Port:")) +o.datatype = "port" +o.placeholder = '80' + +o = s:option(Value, "server_login", translate("Login:")) +o.placeholder = "Device login (ID)" + +o = s:option(Value, "server_password", translate("Password:"), translate("If you don't use Password, leave the field empty")) +o.password = true +o.placeholder = "Device password" + +o = s:option(Flag, "blackbox_enable", translate("BlackBox enable:"), + translate("Blackbox makes it possible to record and store data even in the absence of a cellular signal")) +o:depends("proto","wialon") + +o = s:option(Flag, "blackbox_cycle", translate("BlackBox cycle:"), translate("Cyclic overwriting of data stored in the BlackBox")) +o:depends("proto", "wialon") + +o = s:option(Value, "blackbox_max_size", translate("BlackBox size:"), translate("Number of sentences in the BlackBox")) +o.placeholder = "default: 1000 sentence" +o.datatype = "range(1000, 5000)" +o:depends("proto","wialon") + +o = s:option(DummyValue, "_dummy", translate(" ")) +o.template = packageName .. "/blackbox" +o:depends("proto","wialon") + +o = s:option(Button, "clear", translate("Clear BlackBox"), translate("Warning! After clearing the BlackBox, GNSS data will be destroyed!")) +o.inputstyle = "remove" +o:depends("proto", "wialon") +function o.write(self, section) + local file = io.open("/usr/share/gpoint/tmp/blackbox.json", 'w') + file:write(json.stringify({["size"]=0,["max"]=1000,["data"]={}})) + file:close() +end + + +-- Tab menu settings +s = m:section(TypedSection, "service_settings") +s.addremove = false +s.anonymous = true + + +---------------------------------------------------------------------------------------------------------------- +s:tab("ya", translate("Yandex Locator"), translate("Determines the location of the mobile \ + device by the nearest Wi-Fi access points and \ + cellular base stations — without using satellite navigation systems.")) +s:tab("gpoint_filter", translate("GeoHash Filter"), translate("Filters \"DRIFT\" and \"JUMPS\" of navigation 3G/LTE dongles")) +s:tab("kalman", translate("Kalman Filter"), translate("Designed to make the route smoother. Removes \"jumps\" of navigation 3G/LTE dongles")) + +---------------------------------------------------------------------------------------------------------------- + +-- API Yandex locator +o = s:taboption("ya", Flag, "ya_enable", translate("Enable:"), translate("Enabling the Yandex locator")) +o.optional = true + +o = s:taboption("ya", ListValue, "ya_wifi", translate("Interface:"), translate("Select the Wi-Fi interface for Yandex locator")) +local iwinfo = sys.exec("iwinfo") +no_device = true +for device in string.gmatch(iwinfo, "(%S+)(%s%s%s%s%s)(%S+)") do + o:value(device, device) + no_device = false +end + +if no_device then + o:value('wnf', translate("-- Wifi not found --")) +end + +o = s:taboption("ya", Value, "ya_key", translate("API Key:"), translate("To work with the Yandex locator must use an API key")) +o.password = true +o.placeholder = "Yandex API key" + +o = s:taboption("ya", DummyValue, "ya_href") + function o.cfgvalue(self, section) + local h = "Get Yandex API key" + return translate(h) + end +o.rawhtml = true + +-- GeoHash +o = s:taboption("gpoint_filter", Flag, "filter_enable", translate("Enable:"), translate("Enabling GpointFilter")) +o.optional = true + +o = s:taboption("gpoint_filter", Value, "filter_changes", translate("Jump:"), translate("Registration of the \"jump\" coordinates. \ + The coordinate is recognized as valid after the modem has received it more than the specified number of times.")) +o.placeholder = "" +o.datatype = "range(2, 6)" + +o = s:taboption("gpoint_filter", ListValue, "filter_hash", translate("Area:"), translate("The longer the hash length,\ + the smaller the area and the greater the accuracy of the coordinates in one area.")) +o.optional = true +o.default = 7 +for i = 1, 12 do + o:value(i, i) +end +o = s:taboption("gpoint_filter", Value, "filter_speed", translate("Speed:"), translate("Above the specified speed, the filter will be disabled")) +o.placeholder = "default 2 km/h" +o.datatype = "range(0, 150)" + +-- Kalman +o = s:taboption("kalman", Flag, "kalman_enable", translate("Enable:"), translate("Enabling KalmanFilter")) +o.optional = true +o = s:taboption("kalman", Value, "kalman_noise", translate("Noise:"), translate("Noise is a parameter you can use to alter the expected noise.\ + 1.0 is the original, and the higher it is, the more a path will be \"smoothed\"")) +o.placeholder = "" +o.datatype = "range(1.0, 30.0)" + + +return m diff --git a/luci-app-gpoint-main/luasrc/view/gpoint/blackbox.htm b/luci-app-gpoint-main/luasrc/view/gpoint/blackbox.htm new file mode 100644 index 000000000..6723fb936 --- /dev/null +++ b/luci-app-gpoint-main/luasrc/view/gpoint/blackbox.htm @@ -0,0 +1,33 @@ +<%# + Module for providing data from a mobile satellite navigation system ( mobile modems, etc.) + -= Design and Development 2021-2022 =- + Licensed to the public under the Apache License 2.0. +-%> + +<%+cbi/valueheader%> + +
+ + + +<%+cbi/valuefooter%> diff --git a/luci-app-gpoint-main/luasrc/view/gpoint/buttons.htm b/luci-app-gpoint-main/luasrc/view/gpoint/buttons.htm new file mode 100644 index 000000000..62c2e8e61 --- /dev/null +++ b/luci-app-gpoint-main/luasrc/view/gpoint/buttons.htm @@ -0,0 +1,81 @@ +<%# + Module for providing data from a mobile satellite navigation system ( mobile modems, etc.) + -= Design and Development 2021-2022 =- + Licensed to the public under the Apache License 2.0. +-%> + +<%+gpoint/css/style%> +<%+gpoint/service%> + +<%- + local packageName = "gpoint" + local serviceRunning, serviceEnabled = false, false; + + serviceEnabled = luci.sys.init.enabled(packageName) + local ubusStatus = luci.util.ubus("service", "list", { name = packageName }) + if ubusStatus and ubusStatus[packageName] then + serviceRunning = true + end + + if serviceEnabled then + btn_start_status = true + btn_action_status = true + btn_stop_status = true + btn_enable_status = false + btn_disable_status = true + else + btn_start_status = false + btn_action_status = false + btn_stop_status = false + btn_enable_status = true + btn_disable_status = false + end + if serviceRunning then + btn_start_status = false + btn_action_status = true + btn_stop_status = true + else + btn_action_status = false + btn_stop_status = false + end +-%> + +
+
+ + + + + + +   +   +   +   + + + + +
+
+ +<%-if not btn_start_status then%> + +<%-end%> +<%-if not btn_action_status then%> + +<%-end%> +<%-if not btn_stop_status then%> + +<%-end%> +<%-if not btn_enable_status then%> + +<%-end%> +<%-if not btn_disable_status then%> + +<%-end%> diff --git a/luci-app-gpoint-main/luasrc/view/gpoint/css/style.htm b/luci-app-gpoint-main/luasrc/view/gpoint/css/style.htm new file mode 100644 index 000000000..c6b8ea961 --- /dev/null +++ b/luci-app-gpoint-main/luasrc/view/gpoint/css/style.htm @@ -0,0 +1,26 @@ + diff --git a/luci-app-gpoint-main/luasrc/view/gpoint/js/js.htm b/luci-app-gpoint-main/luasrc/view/gpoint/js/js.htm new file mode 100644 index 000000000..4f917e63d --- /dev/null +++ b/luci-app-gpoint-main/luasrc/view/gpoint/js/js.htm @@ -0,0 +1,183 @@ +<%# + Module for providing data from a mobile satellite navigation system ( mobile modems, etc.) + -= Design and Development 2021-2022 =- + Licensed to the public under the Apache License 2.0. +-%> + + diff --git a/luci-app-gpoint-main/luasrc/view/gpoint/overview.htm b/luci-app-gpoint-main/luasrc/view/gpoint/overview.htm new file mode 100644 index 000000000..4b0e49f84 --- /dev/null +++ b/luci-app-gpoint-main/luasrc/view/gpoint/overview.htm @@ -0,0 +1,140 @@ +<%# + Module for providing data from a mobile satellite navigation system ( mobile modems, etc.) + -= Design and Development 2021-2022 =- + Licensed to the public under the Apache License 2.0. +-%> + +<%+gpoint/css/style%> + +<%+header%> + + + +

+
GPoint: Router Location + +
+

+ +
+
+
+
+
+ +
+

<%:Details:%>

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
<%:Longitude:%>-<%:Latitude:%>-<%:Altitude:%>-
<%:Time (UTC):%>-<%:Date:%>-<%:Satellites:%>-
<%:Hdop:%>-<%:Course:%>-<%:Speed:%>-
+
+
+ + +<%+gpoint/js/js%> + +<%+footer%> diff --git a/luci-app-gpoint-main/luasrc/view/gpoint/service.htm b/luci-app-gpoint-main/luasrc/view/gpoint/service.htm new file mode 100644 index 000000000..d1a9f606d --- /dev/null +++ b/luci-app-gpoint-main/luasrc/view/gpoint/service.htm @@ -0,0 +1,65 @@ +<%# + Module for providing data from a mobile satellite navigation system ( mobile modems, etc.) + -= Design and Development 2021-2022 =- + Licensed to the public under the Apache License 2.0. +-%> + + diff --git a/luci-app-gpoint-main/luasrc/view/gpoint/service_status.htm b/luci-app-gpoint-main/luasrc/view/gpoint/service_status.htm new file mode 100644 index 000000000..f985697c9 --- /dev/null +++ b/luci-app-gpoint-main/luasrc/view/gpoint/service_status.htm @@ -0,0 +1,83 @@ +<%# + Module for providing data from a mobile satellite navigation system ( mobile modems, etc.) + -= Design and Development 2021-2022 =- + Licensed to the public under the Apache License 2.0. +-%> + +<%+cbi/valueheader%> + + + + + + + + + + + + + + + + + + + + + + + + +
-
Application: -
Remote Server: -
Yandex Locator: -
GpointFilter: -
KalmanFilter: -
+ + + +<%+cbi/valuefooter%> diff --git a/luci-app-gpoint-main/root/etc/config/gpoint b/luci-app-gpoint-main/root/etc/config/gpoint new file mode 100644 index 000000000..07f712714 --- /dev/null +++ b/luci-app-gpoint-main/root/etc/config/gpoint @@ -0,0 +1,5 @@ +config modem_settings 'modem_settings' + +config server_settings 'server_settings' + +config service_settings 'service_settings' diff --git a/luci-app-gpoint-main/root/etc/init.d/gpoint b/luci-app-gpoint-main/root/etc/init.d/gpoint new file mode 100644 index 000000000..15322ff85 --- /dev/null +++ b/luci-app-gpoint-main/root/etc/init.d/gpoint @@ -0,0 +1,32 @@ +#!/bin/sh /etc/rc.common +# Copyright 2021-2022 Vladislav Kadulin {spanky@yandex.ru} + +USE_PROCD=1 +START=95 +STOP=01 + +CONFIGURATION=gpoint + +start_service() { + config_load "${CONFIGURATION}" + procd_open_instance + procd_append_param command /usr/share/gpoint/gpoint + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/gpoint.pid + procd_close_instance + echo "gpoint service start" +} + +stop_service() { + if [ -f /var/run/gpoint.pid ]; then + sessionId=$(uci get gpoint.service_settings.sessionid) + uci set gpoint.service_settings.sessionid=stop + uci commit gpoint + ubus call session destroy "{\"ubus_rpc_session\":\"${sessionId}\"}" + echo "gpoint service stop" + else + echo "gpoint not running" + fi +} diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/gpoint b/luci-app-gpoint-main/root/usr/share/gpoint/gpoint new file mode 100644 index 000000000..9961fd7f5 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/gpoint @@ -0,0 +1,229 @@ +#!/usr/bin/env lua +------------------------------------------------------------- +-- luci-app-gpoint. Gnss information dashboard for 3G/LTE dongle. +------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +common_path = "/usr/share/gpoint/modems/?.lua;/usr/share/gpoint/lib/?.lua;/usr/share/gpoint/proto/?.lua;/usr/share/gpoint/lib/kalman_filter/?.lua;" +package.path = common_path .. package.path + +local config = require("config") +local socket = require("socket") +local nixio = require("nixio.fs") +local ubus = require("ubus") + + +function portActive(port) + local fport = nixio.glob("/dev/tty[A-Z][A-Z]*") + for p in fport do + if string.find(p, port) then + return true, {warning = {app = {false, "OK"}}} + end + end + return false, {warning = { + app = {true, "Port is unavailable. Check the modem connections!"}, + locator = {}, + server = {} + } + } +end + +-------------------------------------------------------------------------------- +-- all warnings: [1]-true/false, [2]-what err +-- initializing modem +-------------------------------------------------------------------------------- +local modemVendor = "" +local modemStatus, modemConfig = config.getModemData() +if not modemStatus[1] then + if string.find(modemConfig.name, "Quectel") then + modemVendor = require("que") + elseif string.find(modemConfig.name, "Sierra") then + modemVendor = require("sierra") + elseif string.find(modemConfig.name, "U-Blox") then + modemVendor = require("ublox") + elseif string.find(modemConfig.name, "Simcom") then + modemVendor = require("simcom") + elseif string.find(modemConfig.name, "Dell") then + modemVendor = require("dell") + end +end +-------------------------------------------------------------------------------- +-- initializing Remote Server, Remote Server frequency update time +-------------------------------------------------------------------------------- +local gnssProtocol, frequencyDataSend = "", nil +local serverStatus, serverConfig = config.getServerData() +if not serverStatus[1] then + if serverConfig.protocol == "wialon" then + gnssProtocol = require("wialon_ips") + elseif serverConfig.protocol == "traccar" then + gnssProtocol = require("traccar") + end + frequencyDataSend = os.time() + serverConfig.frequency +end +-------------------------------------------------------------------------------- +-- initializing Yandex Locator +-------------------------------------------------------------------------------- +local locator = "" +local locatorGPSmiss = 0 +local locatorStatus, locatorConfig = config.getLoctorData() +if not locatorStatus[1] then + locator = require("locator") +end +-------------------------------------------------------------------------------- +-- Filter coorinate GeoHash Filter +-------------------------------------------------------------------------------- +local FilterGNSS = "" +local FilterGNSSdata = { + gp = { longitude = "", latitude = "" }, + gga = { longitude = "", latitude = "" }, + gns = { longitude = "", latitude = "" }, + coordHash = 0, + changesSize = 0, + empty = true +} + +local filterStatus, filterConfig = config.getFilterData() +if not filterStatus[1] then + FilterGNSS = require("geohash") +else + FilterGNSS, FilterGNSSdata = nil, nil +end +-------------------------------------------------------------------------------- +-- Filter coorinate KalmanFilter +-------------------------------------------------------------------------------- +local kalman = {} +local kalmanFilter = "" +local kalmanIsStop = true; +local secondsSinceLastUpdate = os.time() + +local kalmanStatus, kalmanConfig = config.getKalmanData() +if not kalmanStatus[1] then + kalmanFilter = require("gps_lib") +else + kalman,kalmanFilter,kalmanIsStop,secondsSinceLastUpdate = nil, nil, nil, nil +end +-------------------------------------------------------------------------------- +-- Config Ubus +-------------------------------------------------------------------------------- +local serviceIsStart = {warning={app={true,"Service start"},locator={true,"Loading..."},server={true,"Loading..."}}} +local conn = ubus.connect() +if not conn then + error("Failed to connect to ubus") +end + +local createUbusSession = conn:call("session", "create", {timeout = 0}) +local ubusSessionId = createUbusSession.ubus_rpc_session +config.setUbusSessionId(ubusSessionId) +conn:call("session", "set", {ubus_rpc_session = ubusSessionId, values = serviceIsStart}) +serviceIsStart = nil +conn:close() +-------------------------------------------------------------------------------- + +local timeToUpdateGNSSdata = 3 + +while true do + -- modem GNSS data + local portStatus, GnssData = portActive(modemConfig.port) + if portStatus then + GnssData = modemVendor.getGNSSdata(modemConfig.port) + if GnssData.warning.app[1] then + modemVendor.start(modemConfig.port) + end + end + + -- yandex locator + GnssData.warning.locator = locatorStatus + if portStatus and not locatorStatus[1] then + if GnssData.gp.longitude == "-" and GnssData.gp.latitude == "-" then + if locatorGPSmiss >= 3 then + local err, latitude, longitude = locator.getLocation(locatorConfig.iface, locatorConfig.key) + GnssData.warning.locator = err[1] and err or {false, "Data received from locator..."} + if not GnssData.warning.locator[1] then + GnssData.gp.latitude = latitude + GnssData.gp.longitude = longitude + GnssData.gp.date = os.date("%d.%m.%Y") + GnssData.gp.utc = os.date("%H:%M", os.time(os.date("!*t"))) -- TODO time + UTF(+3) + GnssData.gga.latitude = locator.degreesToNmea(latitude) + GnssData.gga.longitude = locator.degreesToNmea(longitude) + end + else + GnssData.warning.locator = {true, "getting navigation data..."} + locatorGPSmiss = 1 + locatorGPSmiss + end + else + locatorGPSmiss = 0 + end + end + + -- drift GpointFilter + GnssData.warning.filter = filterStatus + if portStatus and not GnssData.warning.filter[1] then + if not GnssData.warning.gga[1] or not GnssData.warning.gns[1] and GnssData.warning.locator[2] == "OK" then + if GnssData.gp.spkm ~= '-' and tonumber(GnssData.gp.spkm) < filterConfig.speed then + + local tmpHash = FilterGNSS.encode(GnssData.gp.latitude, GnssData.gp.longitude, filterConfig.hash) + + if not FilterGNSSdata.empty and tmpHash == FilterGNSSdata.coordHash and FilterGNSSdata.changesSize < filterConfig.changes then + FilterGNSSdata.changesSize = 0 + GnssData.gp.latitude, GnssData.gp.longitude = FilterGNSSdata.gp.latitude, FilterGNSSdata.gp.longitude + GnssData.gga.latitude, GnssData.gga.longitude = FilterGNSSdata.gga.latitude, FilterGNSSdata.gga.longitude + GnssData.gns.latitude, GnssData.gns.longitude = FilterGNSSdata.gns.latitude, FilterGNSSdata.gns.longitude + + elseif FilterGNSSdata.changesSize >= filterConfig.changes or FilterGNSSdata.empty then + FilterGNSSdata.gp.latitude, FilterGNSSdata.gp.longitude = GnssData.gp.latitude, GnssData.gp.longitude + if not GnssData.warning.gga[1] then + FilterGNSSdata.gga.latitude, FilterGNSSdata.gga.longitude = GnssData.gga.latitude, GnssData.gga.longitude + end + if not GnssData.warning.gns[1] then + FilterGNSSdata.gns.latitude, FilterGNSSdata.gns.longitude = GnssData.gns.latitude, GnssData.gns.longitude + end + FilterGNSSdata.changesSize, FilterGNSSdata.empty, FilterGNSSdata.coordHash = 0, false, tmpHash + else + FilterGNSSdata.changesSize = FilterGNSSdata.changesSize + 1 + end + else + FilterGNSSdata.changesSize, FilterGNSSdata.empty = 0, true + end + end + end + + + -- KalmanFilter + -- TODO enabling and disabling in parking lot + GnssData.warning.kalman = kalmanStatus + if portStatus and not GnssData.warning.kalman[1] then + if not GnssData.warning.gga[1] or not GnssData.warning.gns[1] and GnssData.warning.locator[2] == "OK" then + if kalmanIsStop then + kalman = kalmanFilter.create_velocity2d(kalmanConfig.noise) + kalmanIsStop = false + end + kalman = kalmanFilter.update_velocity2d(kalman, GnssData.gp.latitude, GnssData.gp.longitude, os.time() - secondsSinceLastUpdate) + GnssData.gp.latitude, GnssData.gp.longitude = kalmanFilter.get_lat_lon(kalman) + GnssData.gga.latitude, GnssData.gga.longitude = locator.degreesToNmea(GnssData.gp.latitude), ('0' .. locator.degreesToNmea(GnssData.gp.longitude)) + GnssData.gns.latitude, GnssData.gns.longitude = GnssData.gga.latitude, GnssData.gga.longitude + else + kalmanIsStop = true + end + end + + -- remote server + -- Filter locator to access send data to server + GnssData.warning.server = serverStatus + if locatorGPSmiss == 0 or locatorGPSmiss >= 3 then + if portStatus and not serverStatus[1] and os.time() >= frequencyDataSend then + frequencyDataSend = os.time() + (serverConfig.frequency - timeToUpdateGNSSdata) + GnssData.warning.server = gnssProtocol.sendData(GnssData, serverConfig) + end + end + + -- ubus send json data in session + conn = ubus.connect() + if conn then + conn:call("session", "set", {ubus_rpc_session = ubusSessionId, values = GnssData}) + conn:close() + end + + -- update NMEA coordinate time + socket.sleep((locatorGPSmiss == 0 or locatorGPSmiss >= 3) and timeToUpdateGNSSdata or 1) +end diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/checksum.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/checksum.lua new file mode 100644 index 000000000..6efb81744 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/checksum.lua @@ -0,0 +1,95 @@ +------------------------------------------------------------------- +-- This module is designed to receive a checksum of GNSS messages, +-- Such as crc8 and crc16 checksum. +------------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +checksum = {} + +local function decimalToHex(num) + if num == 0 then + return '0' + end + local neg = false + if num < 0 then + neg = true + num = num * -1 + end + local hexstr = "0123456789ABCDEF" + local result = "" + while num > 0 do + local n = math.mod(num, 16) + result = string.sub(hexstr, n + 1, n + 1) .. result + num = math.floor(num / 16) + end + if neg then + result = '-' .. result + end + return result +end + +local function BitXOR(a, b) + local p, c = 1, 0 + while a > 0 and b > 0 do + local ra, rb = a % 2, b % 2 + if ra ~= rb then c = c + p end + a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 + end + + if a < b then a = b end + while a > 0 do + local ra = a % 2 + if ra > 0 then c = c + p end + a, p = (a - ra) / 2, p * 2 + end + return c +end + +local function BitAND(a, b) + local p, c = 1,0 + while a > 0 and b > 0 do + local ra, rb = a%2, b%2 + if ra + rb > 1 then c = c + p end + a, b, p = (a - ra) / 2, (b - rb) / 2, p*2 + end + return c +end + +local function rshift(x, by) + return math.floor(x / 2 ^ by) +end + + +-- Checksum for NMEA data (CRC8) +function checksum.crc8(data) + local crc8 = string.sub(data, #data - 1) + data = string.sub(data, 2, #data - 3) + + local b_sum = string.byte(data, 1) + for i = 2, #data do + b_sum = BitXOR(b_sum, string.byte(data, i)) + end + + return decimalToHex(b_sum) == crc8 and true or false +end + +-- Checksum for Wialone IPS (CRC16) +function checksum.crc16(s) + assert(type(s) == 'string') + local crc16 = 0x0000 + for i = 1, #s do + local c = s:byte(i) + crc16 = BitXOR(crc16, c) + for j = 1, 8 do + local k = BitAND(crc16, 1) + crc16 = rshift(crc16, 1) + if k ~= 0 then + crc16 = BitXOR(crc16, 0xA001) + end + end + end + return decimalToHex(crc16) +end + +return checksum diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/config.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/config.lua new file mode 100644 index 000000000..86c1fcc8e --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/config.lua @@ -0,0 +1,225 @@ +------------------------------------------------------------------- +-- Module is used for point configuration and interaction with the UI +------------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local uci = require("luci.model.uci") +local sys = require("luci.sys") + +config = {} + +local CFG = uci:get_all("gpoint") + +-- Status table +local STATUS = { + APP = { + MODEM_OK = {false, "OK"}, + MODEM_ERROR = {true, "Modem error. Select modem in the settings!"}, + PORT_ERROR = {true, "Modem Port error. Select port in the settings!"} + }, + SERVER = { + SERVICE_ON = {false, "OK"}, + SERVICE_OFF = {true, "OFF"}, + IP_ERROR = {true, "Server address error. Enter the server address!"}, + PORT_ERROR = {true, "Server port error. Set the server port!"}, + LOGIN_ERROR = {true, "Login (ID) error. Specify the device login!"} + }, + LOCATOR = { + SERVICE_ON = {false, "OK"}, + SERVICE_OFF = {true, "OFF"}, + API_KEY_ERROR = {true, "Yandex Locator: API key not found!"}, + WIFI_IFACE_ERROR = {true, "Yandex Locator: Wi-Fi interface not found!"} + }, + FILTER = { + SERVICE_ON = {false, "OK"}, + SERVICE_OFF = {true, "OFF"} + } +} + +----------------------------------------------------------------------------------- +-- APP (Modem Settings) + -- 1.Checking the configuration for the presence of the modem name + -- 2.Checking the presence of the port in the configuration and whether + -- it is in the list of devices, if the device is unavailable, we return a warning + -- 3. return err + modem data (name modem and NMEA port modem) +----------------------------------------------------------------------------------- +function config.getModemData() + + local err = {} + local modem = { + name = "-", + port = "-" + } + + if not CFG.modem_settings.modem and CFG.modem_settings.modem == "mnf" then + err = STATUS.APP.MODEM_ERROR + elseif CFG.modem_settings.port and CFG.modem_settings.port == "pnf" then + err = STATUS.APP.PORT_ERROR + else + err = STATUS.APP.MODEM_OK + end + + if not err[1] then + modem.name = CFG.modem_settings.modem + modem.port = CFG.modem_settings.port + end + return err, modem +end + +----------------------------------------------------------------------------------- +-- Remote Server + -- 1.We check whether the server service is enabled or not. + -- 2.The correctness of the completed forms is checked such as address, login, port, etc ... + -- 3.We return the absence of an error and the server configuration data otherwise an error, nil ... +----------------------------------------------------------------------------------- +function config.getServerData() + + local err = {} + local server = { + address = "", + port = "", + protocol = "", + login = "", + password = "", + frequency = "", + blackbox = { + enable = "", + cycle = "", + size = 0 + } + } + + if not CFG.server_settings.server_enable then + err = STATUS.SERVER.SERVICE_OFF + elseif not CFG.server_settings.server_ip then + err = STATUS.SERVER.IP_ERROR + elseif not CFG.server_settings.server_port then + err = STATUS.SERVER.PORT_ERROR + elseif not CFG.server_settings.server_login then + err = STATUS.SERVER.LOGIN_ERROR + else + err = STATUS.SERVER.SERVICE_ON + end + + if not err[1] then + server.address = CFG.server_settings.server_ip + server.port = CFG.server_settings.server_port + server.protocol = CFG.server_settings.proto + server.login = CFG.server_settings.server_login + + if server.protocol == "wialon" then + server.password = CFG.server_settings.server_password or "NA" + server.frequency = CFG.server_settings.server_frequency or 5 + server.blackbox.enable = CFG.server_settings.blackbox_enable and true or false + server.blackbox.cycle = CFG.server_settings.blackbox_cycle and true or false + server.blackbox.size = CFG.server_settings.blackbox_max_size or 1000 + elseif server.protocol == "traccar" then + server.frequency = CFG.server_settings.server_frequency or 5 + end + + return err, server + else + return err, nil + end +end + +----------------------------------------------------------------------------------- +-- Yandex Locator + -- 1.Check Yandex Locator service enable/disable + -- 2.Check Yandex API key status enable/disable + -- 3.Check Yandex Locator interface status enable/disable +----------------------------------------------------------------------------------- +function config.getLoctorData() + + local err = {} + local locator = { + enable = false, + iface = "", + key = "" + } + + if not CFG.service_settings.ya_enable then + err = STATUS.LOCATOR.SERVICE_OFF + elseif not CFG.service_settings.ya_key then + err = STATUS.LOCATOR.API_KEY_ERROR + elseif not CFG.service_settings.ya_wifi and CFG.service_settings.ya_wifi == "wnf" then + err = STATUS.LOCATOR.WIFI_IFACE_ERROR + else + err = STATUS.LOCATOR.SERVICE_ON + end + + if not err[1] then + locator.iface = CFG.service_settings.ya_wifi + locator.key = CFG.service_settings.ya_key + return err, locator + else + return err, nil + end +end + +----------------------------------------------------------------------------------- +-- GpointFilter + -- 1. Checking for the filter library + -- 2. Check GpointFilter service enable/disable + -- 3. Make the settings, if there are none, then we apply the default settings +----------------------------------------------------------------------------------- +function config.getFilterData() + + local err = {} + local filter = { + enable = false, + changes = 0, + hash ='0', + speed = 0 + } + + if not CFG.service_settings.filter_enable then + err = STATUS.FILTER.SERVICE_OFF + else + err = STATUS.FILTER.SERVICE_ON + filter.enable = true + filter.changes = tonumber(CFG.service_settings.filter_changes or 3) + filter.hash = tostring(CFG.service_settings.filter_hash or'7') + filter.speed = tonumber(CFG.service_settings.filter_speed or 2) + end + + return err, filter +end + +----------------------------------------------------------------------------------- +-- KalmanFilter + -- 1. Checking for the kalman filter library + -- 2. Check KalmanFilter service enable/disable + -- 3. Make the settings, if there are none, then we apply the default settings +----------------------------------------------------------------------------------- +function config.getKalmanData() + + local err = {} + local filter = { + enable = false, + noise = 0 + } + + if not CFG.service_settings.kalman_enable then + err = STATUS.FILTER.SERVICE_OFF + else + err = STATUS.FILTER.SERVICE_ON + filter.enable = true + filter.noise = tonumber(CFG.service_settings.kalman_noise or 1.0) + end + + return err, filter +end + +----------------------------------------------------------------------------------- +-- Session ID + -- 1.When initializing the ubus, we write the session id to the uci to work with the UI +----------------------------------------------------------------------------------- +function config.setUbusSessionId(id) + uci:set("gpoint", "service_settings", "sessionid", id) + uci:save("gpoint") + uci:commit("gpoint") +end + +return config diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/geohash.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/geohash.lua new file mode 100644 index 000000000..de0d09c2f --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/geohash.lua @@ -0,0 +1,147 @@ +-- Geohash +-- (c) 2015 Ivan Ribeiro Rocha (ivan.ribeiro@gmail.com) +-- (c) 2022 modified by Vladislav Kadulin (spanky@yandex.ru) + +local bit = require("bit32") + +geohash = {} + +local BITS = { 16, 8, 4, 2, 1 } +local BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz" + +local NEIGHBORS = { right = { even = "bc01fg45238967deuvhjyznpkmstqrwx" }, + left = { even = "238967debc01fg45kmstqrwxuvhjyznp" }, + top = { even = "p0r21436x8zb9dcf5h7kjnmqesgutwvy" }, + bottom = { even = "14365h7k9dcfesgujnmqp0r2twvyx8zb" } } + +local BORDERS = { right = { even = "bcfguvyz" }, + left = { even = "0145hjnp" }, + top = { even = "prxz" }, + bottom = { even = "028b" } } + +NEIGHBORS.bottom.odd = NEIGHBORS.left.even +NEIGHBORS.top.odd = NEIGHBORS.right.even +NEIGHBORS.left.odd = NEIGHBORS.bottom.even +NEIGHBORS.right.odd = NEIGHBORS.top.even + +BORDERS.bottom.odd = BORDERS.left.even +BORDERS.top.odd = BORDERS.right.even +BORDERS.left.odd = BORDERS.bottom.even +BORDERS.right.odd = BORDERS.top.even + +function geohash.decode(hash) + local flip = true; + local coords = { latitude = { -90.0, 90.0 }, + longitude = { -180.0, 180.0 } } + + for i = 1, #hash do + local c = hash:sub(i, i) + local cd = BASE32:find(c) - 1 + for j = 1, 5 do + mask = BITS[j] + local tab = (flip and coords.longitude) or coords.latitude + local idx = (bit.band(cd, mask) > 0) and 1 or 2 + tab[idx] = (tab[1] + tab[2]) / 2 + flip = not flip + end + end + + for k, _ in pairs(coords) do + coords[k][3] = (coords[k][1] + coords[k][2]) / 2 + end + + return { lat = coords.latitude, lon = coords.longitude } + +end + +function geohash.encode(latitude, longitude, precision) + local lat = { -90.0, 90.0 } + local lon = { -180.0, 180.0 } + local b, ch, flip = 0, 0, true + local res = ""; + + latitude = tonumber(latitude) + longitude = tonumber(longitude) + precision = tonumber(precision) + local precision = precision or 12 + + while #res < precision do + local tab = flip and lon or lat + local grd = flip and longitude or latitude + + mid = (tab[1] + tab[2]) / 2 + + if grd > mid then + ch = bit.bor(ch, BITS[b + 1]) + tab[1] = mid + else + tab[2] = mid + end + + flip = not flip; + + if b < 4 then + b = b + 1 + else + res = res..BASE32:sub(ch + 1, ch + 1); + b, ch = 0, 0 + end + end + return res +end + +function geohash.calculate_distance(lat1, lon1, lat2, lon2) + local R = 6371000 + local r1, r2 = math.rad(lat1), math.rad(lat2) + local dlat, dlon = math.rad((lat2-lat1)), math.rad((lon2-lon1)) + local a = math.sin(dlat/2) * math.sin(dlat/2) + + math.cos(r1) * math.cos(r2) * + math.sin(dlon/2) * math.sin(dlon/2) + local c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + return R * c +end + +function geohash.distance(hash1, hash2) + local t1, t2 = decode(hash1), decode(hash2) + return calculate_distance(coord(t1).lat, coord(t1).lon, + coord(t2).lat, coord(t2).lon) +end + +function geohash.neighbor(hash, dir) + hash = hash:lower() + local len = #hash + local last = hash:sub(len, len); + local flip = ((math.mod(len,2) == 0) and 'even') or 'odd' + local base = hash:sub(1, len - 1) + if BORDERS[dir][flip]:find(last) then + base = neighbor(base, dir) + end + local n = NEIGHBORS[dir][flip]:find(last) + return base..BASE32:sub(n, n) +end + +function geohash.neighbors(hash) + local neighbors = { top = neighbor(hash, 'top'), + bottom = neighbor(hash, 'bottom'), + right = neighbor(hash, 'right'), + left = neighbor(hash, 'left') } + neighbors.topleft = neighbor(neighbors.left, 'top'); + neighbors.topright = neighbor(neighbors.right, 'top'); + neighbors.bottomleft = neighbor(neighbors.left, 'bottom'); + neighbors.bottomright = neighbor(neighbors.right, 'bottom'); + return neighbors +end + +function geohash.coord(t) + if type(t) == 'table' then + return { lat = t.lat[3], lon = t.lon[3] } + end + return coord(decode(t)) +end + +function geohash.coord_str(t) + local t = coord(t) + return string.format("lat: %s and lon: %s", tostring(t.lat), tostring(t.lon)) +end + +return geohash \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/gps_lib.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/gps_lib.lua new file mode 100644 index 000000000..3e81f5acd --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/gps_lib.lua @@ -0,0 +1,111 @@ +local kalman = require("kalman_lib") +local matrix = require("matrix_lib") + +gps_lib = {} + +local PI = 3.14159265 +local EARTH_RADIUS_IN_MILES = 3963.1676 + +function gps_lib.set_seconds_per_timestep(kalman_filter, seconds_per_timestep) + local unit_scaler = 0.001 + kalman_filter.state_transition[1][3] = unit_scaler * seconds_per_timestep + kalman_filter.state_transition[2][4] = unit_scaler * seconds_per_timestep + return kalman_filter +end + +function gps_lib.create_velocity2d(noise) + local kalman_filter = kalman.create(4, 2) + local v2p = 0.001 + + kalman_filter.state_transition = matrix.set_identity(kalman_filter.state_transition) + kalman_filter = gps_lib.set_seconds_per_timestep(kalman_filter, 1.0) + kalman_filter.observation_model = matrix.set(kalman_filter.observation_model, + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0) + + local pos = 0.000001 + kalman_filter.process_noise_covariance = matrix.set(kalman_filter.process_noise_covariance, + pos, 0.0, 0.0, 0.0, + 0.0, pos, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0) + + kalman_filter.observation_noise_covariance = matrix.set(kalman_filter.observation_noise_covariance, + pos * noise, 0.0, + 0.0, pos * noise) + + kalman_filter.state_estimate = matrix.set(kalman_filter.state_estimate, 0.0, 0.0, 0.0, 0.0) + kalman_filter.estimate_covariance = matrix.set_identity(kalman_filter.estimate_covariance) + local trillion = 1000.0 * 1000.0 * 1000.0 * 1000.0 + kalman_filter.estimate_covariance = matrix.scale(kalman_filter.estimate_covariance, trillion) + return kalman_filter +end + +function gps_lib.update_velocity2d(kalman_filter, lat, lon, seconds_since_last_timestep) + kalman_filter = gps_lib.set_seconds_per_timestep(kalman_filter, seconds_since_last_timestep) + kalman_filter.observation = matrix.set(kalman_filter.observation, lat * 1000.0, lon * 1000.0) + kalman_filter = kalman.update(kalman_filter) + return kalman_filter +end + +function gps_lib.get_lat_lon(kalman_filter) + return string.format("%0.6f", kalman_filter.state_estimate[1][1] / 1000.0), + string.format("%0.6f", kalman_filter.state_estimate[2][1] / 1000.0) +end + +function gps_lib.get_velocity(kalman_filter) + return kalman_filter.state_estimate[3][1] / (1000.0 * 1000.0), + kalman_filter.state_estimate[4][1] / (1000.0 * 1000.0) +end + +function gps_lib.get_bearing(kalman_filter) + local lat, lon = gps_lib.get_lat_lon(kalman_filter) + local delta_lat, delta_lon = gps_lib.get_velocity(kalman_filter) + + local to_radians = PI / 180.0 + lat = lat * to_radians + lon = lon * to_radians + delta_lat = delta_lat * to_radians + delta_lon = delta_lon * to_radians + + local lat1 = lat - delta_lat + local y = math.sin(delta_lon) * math.cos(lat) + local x = math.cos(lat1) * math.sin(lat) - math.sin(lat1) * math.cos(lat) * math.cos(delta_lon) + local bearing = math.atan2(y, x) + + bearing = bearing / to_radians + while bearing >= 360 do + bearing = bearing - 360 + end + while bearing < 0 do + bearing = bearing + 360 + end + return bearing +end + +function gps_lib.calculate_mph(lat, lon, delta_lat, delta_lon) + local to_radians = PI / 180 + lat = lat * to_radians + lon = lon * to_radians + delta_lat = delta_lat * to_radians + delta_lon = delta_lon * to_radians + + local lat1 = lat - delta_lat + local sin_half_dlat = math.sin(delta_lat / 2) + local sin_half_dlon = math.sin(delta_lon / 2) + + local a = sin_half_dlat * sin_half_dlat + math.cos(lat1) * math.cos(lat) * sin_half_dlon * sin_half_dlon + local radians_per_second = 2 * math.atan2(1000 * math.sqrt(a), 1000 * math.sqrt(1.0 - a)) + + local miles_per_second = radians_per_second * EARTH_RADIUS_IN_MILES + local miles_per_hour = miles_per_second * 60 * 60 + return miles_per_hour +end + +function gps_lib.get_mph(kalman_filter) + local lat, lon = gps_lib.get_lat_lon(kalman_filter) + local delta_lat, delta_lon = gps_lib.get_velocity(kalman_filter) + return gps_lib.calculate_mph(lat, lon, delta_lat, delta_lon) +end + +return gps_lib \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/kalman_lib.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/kalman_lib.lua new file mode 100644 index 000000000..207b43007 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/kalman_lib.lua @@ -0,0 +1,82 @@ + local matrix = require("matrix_lib") + +kalman_lib = {} + +function kalman_lib.create(state, observation) + local kalman = { + timestep = 0, -- K + -- These parameters define the size of the matrices. + state_dimension = state, + observation_dimension = observation, + + -- This group of matrices must be specified by the user. + state_transition = matrix.create(state, state), -- F_k + observation_model = matrix.create(observation, state), -- H_k + process_noise_covariance = matrix.create(state, state), -- Q_k + observation_noise_covariance = matrix.create(observation, observation), -- R_k + + -- The observation is modified by the user before every time step. + observation = matrix.create(observation, 1), -- z_k + + -- This group of matrices are updated every time step by the filter. + predicted_state = matrix.create(state, 1), -- x-hat_k|k-1 + predicted_estimate_covariance = matrix.create(state, state), -- P_k|k-1 + innovation = matrix.create(observation, 1), -- y-tilde_k + innovation_covariance = matrix.create(observation, observation), -- S_k + inverse_innovation_covariance = matrix.create(observation, observation), -- S_k^-1 + optimal_gain = matrix.create(state, observation), -- K_k + state_estimate = matrix.create(state, 1), -- x-hat_k|k + estimate_covariance = matrix.create(state, state), -- P_k|k + + -- This group is used for meaningless intermediate calculations. + vertical_scratch = matrix.create(state, observation), + mall_square_scratch = matrix.create(observation, observation), + big_square_scratch = matrix.create(state, state) + } + return kalman +end + +function kalman_lib.predict(kalman) + kalman.timestep = kalman.timestep + 1 + -- Predict the state + kalman.predicted_state = matrix.multiply(kalman.state_transition, kalman.state_estimate, kalman.predicted_state) + -- Predict the state estimate covariance + kalman.big_square_scratch = matrix.multiply(kalman.state_transition, kalman.estimate_covariance, kalman.big_square_scratch) + kalman.predicted_estimate_covariance = matrix.multiply_by_transpose(kalman.big_square_scratch, kalman.state_transition, kalman.predicted_estimate_covariance) + kalman.predicted_estimate_covariance = matrix.add(kalman.predicted_estimate_covariance, kalman.process_noise_covariance, kalman.predicted_estimate_covariance) + return kalman +end + +function kalman_lib.estimate(kalman) + -- Calculate innovation + kalman.innovation = matrix.multiply(kalman.observation_model, kalman.predicted_state, kalman.innovation) + kalman.innovation = matrix.subtract(kalman.observation, kalman.innovation, kalman.innovation) + -- Calculate innovation covariance + kalman.vertical_scratch = matrix.multiply_by_transpose(kalman.predicted_estimate_covariance, kalman.observation_model, kalman.vertical_scratch) + kalman.innovation_covariance = matrix.multiply(kalman.observation_model, kalman.vertical_scratch, kalman.innovation_covariance) + kalman.innovation_covariance = matrix.add(kalman.innovation_covariance, kalman.observation_noise_covariance, kalman.innovation_covariance) + -- Invert the innovation covariance. + -- Note: this destroys the innovation covariance. + -- TODO: handle inversion failure intelligently. + matrix.destructive_invert(kalman.innovation_covariance, kalman.inverse_innovation_covariance) + -- Calculate the optimal Kalman gain. + -- Note we still have a useful partial product in vertical scratch + -- from the innovation covariance. + kalman.optimal_gain = matrix.multiply(kalman.vertical_scratch, kalman.inverse_innovation_covariance, kalman.optimal_gain) + -- Estimate the state + kalman.state_estimate = matrix.multiply(kalman.optimal_gain, kalman.innovation, kalman.state_estimate) + kalman.state_estimate = matrix.add(kalman.state_estimate, kalman.predicted_state, kalman.state_estimate) + -- Estimate the state covariance + kalman.big_square_scratch = matrix.multiply(kalman.optimal_gain, kalman.observation_model, kalman.big_square_scratch) + kalman.big_square_scratch = matrix.subtract_from_identity(kalman.big_square_scratch) + kalman.estimate_covariance = matrix.multiply(kalman.big_square_scratch, kalman.predicted_estimate_covariance, kalman.estimate_covariance) + return kalman +end + +function kalman_lib.update(kalman) + kalman = kalman_lib.predict(kalman) + kalman = kalman_lib.estimate(kalman) + return kalman +end + +return kalman_lib diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/matrix_lib.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/matrix_lib.lua new file mode 100644 index 000000000..23c3dbfc4 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/kalman_filter/matrix_lib.lua @@ -0,0 +1,191 @@ +matrix_lib = {} + +function matrix_lib.create(rows, cols) + local matrix = {} + for i = 1,rows do + matrix[i] = {} + for j = 1,cols do + matrix[i][j] = 0.0 + end + end + return matrix +end + +function matrix_lib.print(matrix) + for i = 1, #matrix do + for j = 1, #matrix[i] do + io.write(matrix[i][j] .. " ") + end + io.write('\n') + end +end + +function matrix_lib.set(matrix, ...) + local k = 1 + for i = 1, #matrix do + for j = 1, #matrix[i] do + if arg[k] ~= nil then + matrix[i][j] = arg[k] + end + k = k + 1 + end + end + return matrix +end + +function matrix_lib.set_identity(matrix) + for i = 1, #matrix do + for j = 1, #matrix[i] do + matrix[i][j] = i == j and 1.0 or 0.0 + end + end + return matrix +end + +function matrix_lib.copy(matrix) + local copy = {} + for i = 1, #matrix do + copy[i] = {} + for j = 1, #matrix[i] do + copy[i][j] = matrix[i][j] + end + end + return copy +end + +function matrix_lib.add(matrix_a, matrix_b, matrix_c) + for i = 1, #matrix_a do + for j = 1, #matrix_a[i] do + matrix_c[i][j] = matrix_a[i][j] + matrix_b[i][j] + end + end + return matrix_c +end + +function matrix_lib.subtract(matrix_a, matrix_b, matrix_c) + for i = 1, #matrix_a do + for j = 1, #matrix_a[i] do + matrix_c[i][j] = matrix_a[i][j] - matrix_b[i][j] + end + end + return matrix_c +end + +function matrix_lib.subtract_from_identity(matrix) + for i = 1, #matrix do + for j = 1, #matrix[i] do + matrix[i][j] = i == j and (1.0 - matrix[i][j]) or (0.0 - matrix[i][j]) + end + end + return matrix +end + +function matrix_lib.multiply(matrix_a, matrix_b, matrix_c) + for i = 1, #matrix_c do + for j = 1, #matrix_c[i] do + matrix_c[i][j] = 0.0 + for k = 1, #matrix_a[i] do + matrix_c[i][j] = matrix_c[i][j] + (matrix_a[i][k] * matrix_b[k][j]) + end + end + end + return matrix_c +end + +function matrix_lib.multiply_by_transpose(matrix_a, matrix_b, matrix_c) + for i = 1, #matrix_c do + for j = 1, #matrix_c[i] do + matrix_c[i][j] = 0.0 + for k = 1, #matrix_a[1] do + matrix_c[i][j] = matrix_c[i][j] + (matrix_a[i][k] * matrix_b[j][k]) + end + end + end + return matrix_c +end + +function matrix_lib.transpose(matrix_input, matrix_output) + for i = 1, #matrix_input do + for j = 1, #matrix_input[i] do + matrix_output[j][i] = matrix_input[i][j] + end + end + return matrix_output +end + +function matrix_lib.equal(matrix_a, matrix_b, tolerance) + for i = 1, #matrix_a do + for j = 1, #matrix_a[i] do + if math.abs(matrix_a[i][j] - matrix_b[i][j]) > tolerance then + return false + end + end + end + return true +end + +function matrix_lib.scale(matrix, scalar) + for i = 1, #matrix do + for j = 1, #matrix[i] do + matrix[i][j] = matrix[i][j] * scalar + end + end + return matrix +end + +function matrix_lib.swap_rows(matrix, r1, r2) + local tmp = matrix[r1] + matrix[r1] = matrix[r2] + matrix[r2] = tmp + return matrix +end + +function matrix_lib.scale_row(matrix, r, scalar) + for i = 1, #matrix do + matrix[r][i] = matrix[r][i] * scalar + end + return matrix +end + +function matrix_lib.shear_row(matrix, r1, r2, scalar) + for i = 1, #matrix do + matrix[r1][i] = matrix[r1][i] + (scalar * matrix[r2][i]) + end + return matrix +end + +function matrix_lib.destructive_invert(matrix_input, matrix_output) + matrix_output = matrix_lib.set_identity(matrix_output) + for i = 1, #matrix_input do + if matrix_input[i][i] == 0.0 then + local j + for j = i + 1, #matrix_input do + if matrix_input[r][i] ~= 0.0 then + return + end + end + + if j == #matrix_input then + return + end + + matrix_input = matrix_lib.swap_rows(matrix_input, i, j) + matrix_output = matrix_lib.swap_rows(matrix_output, i, j) + end + + local scalar = 1.0 / matrix_input[i][i] + matrix_input = matrix_lib.scale_row(matrix_input, i, scalar) + matrix_output = matrix_lib.scale_row(matrix_output, i, scalar) + + for r = 1, #matrix_input do + if i ~= r then + local shear_needed = -matrix_input[r][i] + matrix_input = matrix_lib.shear_row(matrix_input, r, i, shear_needed) + matrix_output = matrix_lib.shear_row(matrix_output, r, i, shear_needed) + end + end + end + return matrix_input, matrix_output +end + +return matrix_lib \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/locator.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/locator.lua new file mode 100644 index 000000000..cea13b093 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/locator.lua @@ -0,0 +1,78 @@ +------------------------------------------------------------------- +-- Module is designed to work with the Yandex Locator API +-- (WiFi is required!) +------------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local json = require("luci.jsonc") +local sys = require("luci.sys") +local iwinfo = require("iwinfo") + +locator = {} + +local function configJSON(jsonData, iface, key) + jsonData.common.api_key = key + local inter = iwinfo.type(iface) + local scanlist = iwinfo[inter].scanlist(iface) + for _, v in pairs(scanlist) do + v.bssid = string.gsub(v.bssid, ':', '') + table.insert(jsonData.wifi_networks, {["mac"] = v.bssid, ["signal_strength"] = v.signal}) + end +end + +local function request(curl, jsonData) + curl = curl .. json.stringify(jsonData) .. '\'' + local res = sys.exec(curl) + if res == "" then + res = "{\"error\": {\"message\":\"No internet connection\"}}" + end + return json.parse(res) +end + +-- Converter from degrees to NMEA data. +function locator.degreesToNmea(coord) + local degrees = math.floor(coord) + coord = math.abs(coord) - degrees + local sign = coord < 0 and "-" or "" + return sign .. string.format("%02i%02.5f", degrees, coord * 60.00) +end + +-- Getting data coordinates via Yandex API +function locator.getLocation(iface_name, api_key) + local curl = "curl -X POST 'http://api.lbs.yandex.net/geolocation' -d 'json=" + local jsonData = { + wifi_networks = {}, + common = { + version = "1.0", + api_key = "" + } + } + + configJSON(jsonData, iface_name, api_key) + local location = request(curl, jsonData) + local err = {false, "OK"} + local latitude = "" + local longitude = "" + local altitude = "" + + if location.error then + err = {true, location.error.message} + end + + if location.position then + if tonumber(location.position.precision) >= 100000 then + err = {true, "Bad precision"} + else + latitude = string.format("%0.8f", location.position.latitude) + longitude = string.format("%0.8f", location.position.longitude) + if latitude == "" or longitude == "" then + err = {true, "Bad data..."} + end + end + end + + return err, latitude, longitude +end + +return locator diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/nmea.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/nmea.lua new file mode 100644 index 000000000..698ab8000 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/nmea.lua @@ -0,0 +1,385 @@ +------------------------------------------------------------- +-- This module is designed to extract data from NMEA messages. +-- All data is combined into a table "GnssData". +------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local uci = require("luci.model.uci") +local serial = require("serial") +local checksum = require("checksum") + + +local nmea = {} + +-- Table for navigation data +local function createGnssForm() + local GnssForm = { + warning = { + app = {true, ""}, + gga = {true, ""}, + rmc = {true, ""}, + vtg = {true, ""}, + gsa = {true, ""}, + gp = {true, ""}, + gns = {true, ""}, + server = {true, ""}, + locator = {true, ""} + }, + gp = { longitude = "-", latitude = "-"}, + gga = { longitude = "-", latitude = "-"} + } + return GnssForm +end + +--Converting coordinates from the NMEA protocol to degrees +local function nmeaCoordinatesToDouble(coord) + local deg = math.floor(coord / 100) + return deg + (coord - 100 * deg) / 60 +end + +--We are looking for the desired data line in the line received from the device +local function findInResp(data, begin) + local err = true + local b = string.find(data, begin) + local e = string.find(data, "\r\n", b) + + if b and e then + err = false + else + b, e = nil, nil + end + return err, b, e +end + +-- message parsing, checksum checking +local function getCropData(data, msg) + local err, b, e = findInResp(data, msg) + if not err then + data = string.gsub(string.sub(data, b, e), '%c', "") + if checksum.crc8(data) then + data = string.gsub(data, msg, '', 1) + data = string.gsub(data, "*%d+%w+", '', 1) + err = {false, "OK"} + else + err = {true, "Checksum error"} + data = nil + end + else + err = {true, "No data found"} + data = nil + end + return err, data +end + +-- Creating a table with data before adding data to a single space +function doTable(data, keys) + local parseData = {} + + while string.find(data, ',,') do + data = string.gsub(data, ',,', ",-,") + end + + if string.sub(data, 1, 1) == ',' then + data = '-' .. data + end + + local i = 1 + for val in string.gmatch(data, "[^,]+") do + parseData[keys[i]] = val + i = i + 1 + end + return parseData +end + +-- The function of searching the time zone by the received coordinates +local function findTimeZone(time, date, lon) + local datetime = { year,month,day,hour,min,sec } + local timeZone = uci:get("gpoint", "modem_settings", "timezone") + + -- calculate the time zone by coordinates + if timeZone == nil or timeZone == "auto" then + timeZone = math.floor((tonumber(lon) + (7.5 * (tonumber(lon) > 0 and 1.0 or -1.0))) / 15.0) + end + + datetime.hour, datetime.min, datetime.sec = string.match(time, "(%d%d)(%d%d)(%d%d)") + datetime.day, datetime.month, datetime.year = string.match(date,"(%d%d)(%d%d)(%d%d)") + datetime.year = "20" .. datetime.year -- Someone change this to 21 in the 2100 year + + --we request the unix time and then add the time zone + local unix = os.time(datetime) + unix = unix + ((math.floor(tonumber(timeZone) * 100)) % 100) * 36 + return unix + math.floor(tonumber(timeZone)) * 3600 +end + +-- Add 0 for the time and date values if < 10 +local function addZero(val) + return tonumber(val) > 9 and tostring(val) or '0' .. tostring(val) +end + +-- If there is no data, the default values of the table are dashed +local function addDash(data) + local dashData = {} + for i=1, #data do + dashData[data[i]] = '-' + end + return dashData +end + +--------------------------------------------------------------------------------------------------------------- +-- GGA - Global Positioning System Fix Data +local function getGGA(GnssData, resp) + GnssData.gga = { + "utc", -- UTC of this position report, hh is hours, mm is minutes, ss.ss is seconds. + "latitude", -- Latitude, dd is degrees, mm.mm is minutes + "ne", -- N or S (North or South) + "longitude", -- Longitude, dd is degrees, mm.mm is minutes + "ew", -- E or W (East or West) + "qual", -- GPS Quality Indicator (non null) + "sat", -- Number of satellites in use, 00 - 12 + "hdp", -- Horizontal Dilution of precision (meters) + "alt", -- Antenna Altitude above/below mean-sea-level (geoid) (in meters) + "ualt", -- Units of antenna altitude, meters + "gsep", -- Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level + "ugsep", -- Units of geoidal separation, meters + "age", -- Age of differential GPS data, time in seconds since last SC104 type 1 or 9 update, null field when DGPS is not used + "drs" -- Differential reference station ID, 0000-1023 + } + + local err, gga = getCropData(resp, "$GPGGA,") + if not err[1] and string.gsub(gga, ',', '') ~= '0' + and string.sub(gga, string.find(gga, ',') + 1, string.find(gga, ',') + 1) ~= ',' then + GnssData.gga = doTable(gga, GnssData.gga) + GnssData.warning.gga = {false, "OK"} + else + GnssData.gga = addDash(GnssData.gga) + GnssData.warning.gga = err[1] and err or {true, "Bad GGA data"} + end +end + +-- RMC - Recommended Minimum Navigation Information +local function getRMC(GnssData, resp) + GnssData.rmc = { + "utc", -- UTC of position fix, hh is hours, mm is minutes, ss.ss is seconds. + "valid", -- Status, A = Valid, V = Warning + "latitude", -- Latitude, dd is degrees. mm.mm is minutes. + "ns", -- N or S + "longitude", -- Longitude, ddd is degrees. mm.mm is minutes. + "ew", -- E or W + "knots", -- Speed over ground, knots + "tmgdt", -- Track made good, degrees true + "date", -- Date, ddmmyy + "mv", -- Magnetic Variation, degrees + "ewm", -- E or W + "nstat", -- Nav Status (NMEA 4.1 and later) A=autonomous, D=differential, E=Estimated, -> + -- M=Manual input mode N=not valid, S=Simulator, V = Valid + "sc" --checksum + } + + local err, rmc = getCropData(resp, "$GPRMC,") + if not err[1] and string.find(rmc, ",A,") then + GnssData.rmc = doTable(rmc, GnssData.rmc) + GnssData.warning.rmc = {false, "OK"} + else + GnssData.rmc = addDash(GnssData.rmc) + GnssData.warning.rmc = err[1] and err or {true, "Bad RMC data"} + end +end + +-- VTG - Track made good and Ground speed +local function getVTG(GnssData, resp) + GnssData.vtg = { + "course_t", -- Course over ground, degrees True + 't', -- T = True + "course_m", -- Course over ground, degrees Magnetic + 'm', -- M = Magnetic + "knots", -- Speed over ground, knots + 'n', -- N = Knots + "speed", -- Speed over ground, km/hr + 'k', -- K = Kilometers Per Hour + "faa" -- FAA mode indicator (NMEA 2.3 and later) + } + + local err, vtg = getCropData(resp, "$GPVTG,") + if not err[1] and (string.find(vtg, 'A') or string.find(vtg, 'D')) then + GnssData.vtg = doTable(vtg, GnssData.vtg) + GnssData.warning.vtg = {false, "OK"} + else + GnssData.vtg = addDash(GnssData.vtg) + GnssData.warning.vtg = err[1] and err or {true, "Bad VTG data"} + end +end + +--GSA - GPS DOP and active satellites +local function getGSA(GnssData, resp) + GnssData.gsa = { + "smode", -- Selection mode: M=Manual, forced to operate in 2D or 3D, A=Automatic, 2D/3D + "mode", -- Mode (1 = no fix, 2 = 2D fix, 3 = 3D fix) + "id1", -- ID of 1st satellite used for fix + "id2", -- ID of 2nd satellite used for fix + "id3", -- ID of 3rd satellite used for fix + "id4", -- ID of 4th satellite used for fix + "id5", -- ID of 5th satellite used for fix + "id6", -- ID of 6th satellite used for fix + "id7", -- ID of 7th satellite used for fix + "id8", -- ID of 8th satellite used for fix + "id9", -- ID of 9th satellite used for fix + "id10", -- ID of 10th satellite used for fix + "id11", -- ID of 11th satellite used for fix + "id12", -- ID of 12th satellite used for fix + "pdop", -- PDOP + "hdop", -- HDOP + "vdop", -- VDOP + "sc" -- checksum + } + + local err, gsa = getCropData(resp, "$GPGSA,") + if not err[1] and string.find(gsa, '2') then + GnssData.gsa = doTable(gsa, GnssData.gsa) + GnssData.warning.gsa = {false, "OK"} + else + GnssData.gsa = addDash(GnssData.gsa) + GnssData.warning.gsa = err[1] and err or {true, "Bad GSA data"} + end +end + +--GNS - GLONAS Fix data +local function getGNS(GnssData, resp) + GnssData.gns = { + "utc", -- UTC of this position report, hh is hours, mm is minutes, ss.ss is seconds. + "latitude", -- Latitude, dd is degrees, mm.mm is minutes + "ne", -- N or S (North or South) + "longitude", -- Longitude, dd is degrees, mm.mm is minutes + "ew", -- E or W (East or West) + "mi", -- Mode indicator (non-null) + "sat", -- Total number of satellites in use, 00-99 + "hdp", -- Horizontal Dilution of Precision, HDOP + "alt", -- Antenna Altitude above/below mean-sea-level (geoid) (in meters) + "gsep", -- Goeidal separation meters + "age", -- Age of differential data + "drs", -- Differential reference station ID + "nstat" --Navigational status (optional) S = Safe C = Caution U = Unsafe V = Not valid for navigation + } + + local err, gns = getCropData(resp, "$GNGNS,") + if not err[1] and string.gsub(gns, ',', '') ~= '0' and not string.find(gns, "NNN") then + GnssData.gns = doTable(gns, GnssData.gns) + GnssData.warning.gns = {false, "OK"} + else + GnssData.gns = addDash(GnssData.gns) + GnssData.warning.gns = err[1] and err or {true, "Bad GNS data"} + end +end + +-- Prepares data for the web application (Some custom data) +local function getGPoint(GnssData, resp) + GnssData.gp = { + longitude = '-', + latitude = '-', + altitude = '-', + utc = '-', + date = '-', + nsat = '-', + hdop = '-', + cog = '-', + spkm = '-', + unix = '-' + } + + local err = {true, ""} + local GpsOrGlonas = false + + if not GnssData.warning.gga[1] then + GpsOrGlonas = GnssData.gga + elseif not GnssData.warning.gns[1] then + GpsOrGlonas = GnssData.gns + else + err[2] = "GGA: " .. GnssData.warning.gga[2] .. ' ' .. "GNS: " .. GnssData.warning.gns[2] .. ' ' + end + + if GpsOrGlonas then + GnssData.gp.latitude = string.format("%0.6f", nmeaCoordinatesToDouble(GpsOrGlonas.latitude)) + GnssData.gp.longitude = string.format("%0.6f", nmeaCoordinatesToDouble(GpsOrGlonas.longitude)) + GnssData.gp.altitude = GpsOrGlonas.alt + GnssData.gp.nsat = GpsOrGlonas.sat + GnssData.gp.hdop = GpsOrGlonas.hdp + end + + if not GnssData.warning.vtg[1] then + GnssData.gp.cog = GnssData.vtg.course_t + GnssData.gp.spkm = GnssData.vtg.speed + else + err[2] = err[2] .. "VTG: " .. GnssData.warning.vtg[2] .. ' ' + end + + if not GnssData.warning.rmc[1] then + local unixTime = findTimeZone(GnssData.rmc.utc, GnssData.rmc.date, nmeaCoordinatesToDouble(GnssData.rmc.longitude)) + local dateTime = os.date("*t", unixTime) + + GnssData.gp.utc = string.format("%s:%s", addZero(dateTime.hour), addZero(dateTime.min)) + GnssData.gp.date = string.format("%s.%s.%d", addZero(dateTime.day), addZero(dateTime.month), dateTime.year) + GnssData.gp.unix = unixTime + else + err[2] = err[2] .. "RMC: " .. GnssData.warning.rmc[2] + end + + if GnssData.warning.gga[1] and GnssData.warning.gns[1] and GnssData.warning.vtg[1] and GnssData.warning.rmc[1] then + err = {false, "Updating data..."} + end + + if err[2] == "" then + err = {false, "OK"} + end + + GnssData.warning.gp = err +end + +------------------------------------------------------ +-- Get a certain kind of NMEA data (data parsing) +------------------------------------------------------ + +function nmea.getData(line, port) + GnssData = createGnssForm() + GnssData.warning.app, resp = serial.read(port) + + if line == "GP" then + getGGA(GnssData, resp) + getGNS(GnssData, resp) + getRMC(GnssData, resp) + getVTG(GnssData, resp) + getGPoint(GnssData, resp) + elseif line == "GGA" then + getGGA(GnssData, resp) + elseif line == "GNS" then + getGNS(GnssData, resp) + elseif line == "RMC" then + getRMC(GnssData, resp) + elseif line == "VTG" then + getVTG(GnssData, resp) + elseif line == "GSA" then + getGSA(GnssData, resp) + else + GnssData.warning.app = {true, "Bad argument..."} + end + return GnssData +end + +------------------------------------------------------ +-- parsing all NMEA data +------------------------------------------------------ + +function nmea.getAllData(port) + GnssData = createGnssForm() + GnssData.warning.app, resp = serial.read(port) + + getGGA(GnssData, resp) + getGNS(GnssData, resp) + getRMC(GnssData, resp) + getVTG(GnssData, resp) + getGSA(GnssData, resp) -- rarely used + getGPoint(GnssData, resp) + + return GnssData +end + +return nmea diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/lib/serial.lua b/luci-app-gpoint-main/root/usr/share/gpoint/lib/serial.lua new file mode 100644 index 000000000..ca6dc1381 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/lib/serial.lua @@ -0,0 +1,73 @@ +------------------------------------------------------------------- +-- Wrapper for working with a modem via serial port +------------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local rs232 = require("luars232") + +local serial = {} + +local function configSerial(port) + assert(port:set_baud_rate(rs232.RS232_BAUD_115200) == rs232.RS232_ERR_NOERROR) + assert(port:set_parity(rs232.RS232_PARITY_NONE) == rs232.RS232_ERR_NOERROR) + assert(port:set_data_bits(rs232.RS232_DATA_8) == rs232.RS232_ERR_NOERROR) + assert(port:set_stop_bits(rs232.RS232_STOP_1) == rs232.RS232_ERR_NOERROR) + assert(port:set_flow_control(rs232.RS232_FLOW_OFF) == rs232.RS232_ERR_NOERROR) +end + +-- write data from modem (AT PORT) +function serial.write(serial_port, command) + local err, port = rs232.open(serial_port) + if err ~= rs232.RS232_ERR_NOERROR then + err = {true, "Error opening AT port"} + assert(port:close() == rs232.RS232_ERR_NOERROR) + return err + end + configSerial(port) + + local err, len_written = port:write(command .. "\r\n") + if err ~= rs232.RS232_ERR_NOERROR then + err = {true, "Error writing AT port"} + assert(port:close() == rs232.RS232_ERR_NOERROR) + return err + end + + err = {false, "OK"} + assert(port:close() == rs232.RS232_ERR_NOERROR) + return err +end + +-- read data from modem (GNSS PORT) +function serial.read(serial_port) + local err, port = rs232.open(serial_port) + if err ~= rs232.RS232_ERR_NOERROR then + err = {true, "Error opening GNSS port"} + assert(port:close() == rs232.RS232_ERR_NOERROR) + return err, '' + end + configSerial(port) + + local READ_LEN = 1024 -- Read byte form GNSS port + local TIMEOUT = 1000 -- Timeout reading in miliseconds + + local serialData, err, read_data = {}, "", "" + while READ_LEN > 0 do + err, read_data = port:read(1, TIMEOUT) + if err ~= rs232.RS232_ERR_NOERROR then + err = {true, "Error reading GNSS port. Updating data or searching for satellites..."} + assert(port:close() == rs232.RS232_ERR_NOERROR) + return err, "" + end + if read_data ~= nil then + table.insert(serialData, read_data) + READ_LEN = READ_LEN - 1 + end + end + assert(port:close() == rs232.RS232_ERR_NOERROR) + + err = {false, "OK"} + return err, table.concat(serialData) +end + +return serial diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/modems/dell.lua b/luci-app-gpoint-main/root/usr/share/gpoint/modems/dell.lua new file mode 100644 index 000000000..a28b63dfe --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/modems/dell.lua @@ -0,0 +1,44 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + + +local nmea = require("nmea") +local serial = require("serial") +local nixio = require("nixio.fs") + +local dell = {} + +local DELL_BEGIN_GPS = "AT+GPS=1" +local DELL_END_GPS = "AT+GPS=0" + +-- automatic activation of the NMEA port for data transmission +function dell.start(port) + local p = tonumber(string.sub(port, #port)) + 1 + p = string.gsub(port, '%d', tostring(p)) + local error, resp = true, { + warning = { + app = {true, "Port is unavailable. Check the modem connections!"}, + locator = {}, + server = {} + } + } + -- DELL DW5821 series default NMEA /dev/ttyUSB2 + local fport = nixio.glob("/dev/tty[A-Z][A-Z]*") + for name in fport do + if string.find(name, p) then + error, resp = serial.write(p, DELL_BEGIN_GPS) + end + end + return error, resp +end +-- stop send data to NMEA port +function dell.stop(port) + error, resp = serial.write(port, DELL_END_GPS) + return error, resp +end +-- get GNSS data for application +function dell.getGNSSdata(port) + return nmea.getAllData(port) +end + +return dell diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/modems/que.lua b/luci-app-gpoint-main/root/usr/share/gpoint/modems/que.lua new file mode 100644 index 000000000..f837ca90e --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/modems/que.lua @@ -0,0 +1,41 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + +local nmea = require("nmea") +local serial = require("serial") +local nixio = require("nixio.fs") + +local que = {} + +local QUECTEL_BEGIN_GPS = "AT+QGPS=1" +local QUECTEL_END_GPS = "AT+QGPSEND" + +-- automatic activation of the NMEA port for data transmission +function que.start(port) + local p = tonumber(string.sub(port, #port)) + 1 + p = string.gsub(port, '%d', tostring(p)) + local error, resp = true, {warning = { + app = {true, "Port is unavailable. Check the modem connections!"}, + locator = {}, + server = {} + } + } + local fport = nixio.glob("/dev/tty[A-Z][A-Z]*") + for name in fport do + if string.find(name, p) then + error, resp = serial.write(p, QUECTEL_BEGIN_GPS) + end + end + return error, resp +end +-- stop send data to NMEA port +function que.stop(port) + error, resp = serial.write(port, QUECTEL_END_GPS) + return error, resp +end +-- get GNSS data for application +function que.getGNSSdata(port) + return nmea.getAllData(port) +end + +return que \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/modems/sierra.lua b/luci-app-gpoint-main/root/usr/share/gpoint/modems/sierra.lua new file mode 100644 index 000000000..5d0ba6cae --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/modems/sierra.lua @@ -0,0 +1,40 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + + +local nmea = require("nmea") +local serial = require("serial") +local nixio = require("nixio.fs") + +local sierra = {} + +local SIERRA_BEGIN_GPS = "$GPS_START" +local SIERRA_END_GPS = "$GPS_STOP" + +-- automatic activation of the NMEA port for data transmission +function sierra.start(port) + local error, resp = true, {warning = { + app = {true, "Port is unavailable. Check the modem connections!"}, + locator = {}, + server = {} + } + } + local fport = nixio.glob("/dev/tty[A-Z][A-Z]*") + for name in fport do + if string.find(name, port) then + error, resp = serial.write(port, SIERRA_BEGIN_GPS) + end + end + return error, resp +end +-- stop send data to NMEA port +function sierra.stop(port) + error, resp = serial.write(port, SIERRA_END_GPS) + return error, resp +end +-- get GNSS data for application +function sierra.getGNSSdata(port) + return nmea.getAllData(port) +end + +return sierra \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/modems/simcom.lua b/luci-app-gpoint-main/root/usr/share/gpoint/modems/simcom.lua new file mode 100644 index 000000000..02319b4e6 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/modems/simcom.lua @@ -0,0 +1,44 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + + +local nmea = require("nmea") +local serial = require("serial") +local nixio = require("nixio.fs") + +local simcom = {} + +local SIMCOM_BEGIN_GPS = "AT+CGPS=1,1" +local SIMCOM_END_GPS = "AT+CGPS=0,1" + +-- automatic activation of the NMEA port for data transmission +function simcom.start(port) + local p = tonumber(string.sub(port, #port)) + 1 + p = string.gsub(port, '%d', tostring(p)) + local error, resp = true, { + warning = { + app = {true, "Port is unavailable. Check the modem connections!"}, + locator = {}, + server = {} + } + } + -- SIM7600 series default NMEA /dev/ttyUSB1 + local fport = nixio.glob("/dev/tty[A-Z][A-Z]*") + for name in fport do + if string.find(name, p) then + error, resp = serial.write(p, SIMCOM_BEGIN_GPS) + end + end + return error, resp +end +-- stop send data to NMEA port +function simcom.stop(port) + error, resp = serial.write(port, SIMCOM_END_GPS) + return error, resp +end +-- get GNSS data for application +function simcom.getGNSSdata(port) + return nmea.getAllData(port) +end + +return simcom diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/modems/ublox.lua b/luci-app-gpoint-main/root/usr/share/gpoint/modems/ublox.lua new file mode 100644 index 000000000..9bf3898fa --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/modems/ublox.lua @@ -0,0 +1,22 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + + +local nmea = require("nmea") + +local ublox = {} + +-- Wrapper over the interface, the module does not need an implementation +function ublox.start(port) + return false, {warning = {app = {false, "GOOD!"}, locator = {}, server = {}}} +end + +function ublox.stop(port) + return {false, "OK"} +end +-- get GNSS data for application +function ublox.getGNSSdata(port) + return nmea.getAllData(port) +end + +return ublox \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/proto/traccar.lua b/luci-app-gpoint-main/root/usr/share/gpoint/proto/traccar.lua new file mode 100644 index 000000000..148da57f7 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/proto/traccar.lua @@ -0,0 +1,31 @@ +------------------------------------------------------------- +-- Traccar Client use this protocol to report GPS data to the server side. +-- OsmAnd Live Tracking web address format: +-- http://demo.traccar.org:5055/?id=123456&lat={0}&lon={1}×tamp={2}&hdop={3}&altitude={4}&speed={5} +------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local http = require("socket.http") + +local trackcar = {} + +local function OsmAnd(GnssData, serverConfig) + local unix = GnssData.warning.rmc[1] and os.time() or GnssData.gp.unix + return string.format("http://%s:%s/?id=%s&lat=%s&lon=%s×tamp=%s&hdop=%s&altitude=%s&speed=%s&satellites=%s", + serverConfig.address, serverConfig.port, serverConfig.login, + GnssData.gp.latitude or '-', GnssData.gp.longitude or '-', + unix or '-', GnssData.gp.hdop or '-', + GnssData.gp.altitude or '-', GnssData.gp.spkm or '-', + GnssData.gp.nsat or '-') +end + +-- Send data to server side +function trackcar.sendData(GnssData, serverConfig) + local data = OsmAnd(GnssData, serverConfig) + http.TIMEOUT = 0.5 + http.request{ method = "POST", url = data} + return {false, "OK"} +end + +return trackcar \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/proto/wialon_ips.lua b/luci-app-gpoint-main/root/usr/share/gpoint/proto/wialon_ips.lua new file mode 100644 index 000000000..f6ffedd76 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/proto/wialon_ips.lua @@ -0,0 +1,160 @@ +------------------------------------------------------------- +-- A module for working with the "WIALON IPS" navigation protocol. +-- This module prepares and sends data to a remote server. +------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local json = require("luci.jsonc") +local checksum = require("checksum") +local blackbox = require("wialon_ips_boof") +local socket = require("socket") +local tcp = assert(socket.tcp()) + +local wialon_ips = {} + +-- Abbreviated data package +local function shortData(GnssData) + local SD = {"NA","NA","NA","NA","NA","NA","NA","NA","NA","NA"} + if not GnssData.warning.rmc[1] then + SD[1], SD[2] = GnssData.rmc.date, GnssData.rmc.utc + end + if not GnssData.warning.gga[1] then + -- Lat2[4], Lon2[6] - NA + SD[3], SD[5] = GnssData.gga.latitude, GnssData.gga.longitude + SD[9], SD[10] = GnssData.gga.alt, GnssData.gga.sat + elseif not GnssData.warning.gns[1] then + SD[3], SD[5] = GnssData.gns.latitude, GnssData.gns.longitude + SD[9], SD[10] = GnssData.gns.alt, GnssData.gns.sat + elseif not GnssData.warning.locator[1] then + SD[1], SD[2] = os.date("%d%m%y"), os.date("%H%M%S", os.time(os.date("!*t"))) .. ".00" + SD[3], SD[5] = GnssData.gga.latitude, '0' .. GnssData.gga.longitude + end + if not GnssData.warning.vtg[1] then + SD[7], SD[8] = GnssData.vtg.speed, GnssData.vtg.course_t + end + SD[11] = checksum.crc16(table.concat(SD, ";") .. ';') + return "#SD#" .. table.concat(SD, ";") .. "\r\n" +end + +-- Extended Data package with CRC16 +local function bigData(GnssData, params) + local D = {"NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","","NA","NA"} + if not GnssData.warning.rmc[1] then + D[1], D[2] = GnssData.rmc.date, GnssData.rmc.utc + end + if not GnssData.warning.gga[1] then + -- Lat2[4], Lon2[6] - NA + D[3], D[5] = GnssData.gga.latitude, GnssData.gga.longitude + D[9], D[10] = GnssData.gga.alt, GnssData.gga.sat + D[11] = GnssData.gga.hdp + elseif not GnssData.warning.gns[1] then + D[3], D[5] = GnssData.gns.latitude, GnssData.gns.longitude + D[9], D[10] = GnssData.gns.alt, GnssData.gns.sat + D[11] = GnssData.gns.hdp + elseif not GnssData.warning.locator[1] then + D[1], D[2] = os.date("%d%m%y"), os.date("%H%M%S", os.time(os.date("!*t"))) .. ".00" + D[3], D[5] = GnssData.gga.latitude, '0' .. GnssData.gga.longitude + D[16] = string.format("%s:%s:%s", "yandex", '3', "Data from wi-fi scan") + end + if not GnssData.warning.vtg[1] then + D[7], D[8] = GnssData.vtg.speed, GnssData.vtg.course_t + end + if params then + -- TODO PARAMS WITH YA LOCATOR + D[16] = string.format("%s:%s:%s", params[1], params[2], params[3]) + end + D[17] = checksum.crc16(table.concat(D, ";") .. ';') + return "#D#" .. table.concat(D, ";") .. "\r\n" +end + +-- Response from the server to the message +local function handlErr(resp) + local ERROR_CODE = { + ["#AL#1"] = "OK", + ["#ASD#1"] = "OK", + ["#AD#1"] = "OK", + ["#AL#0"] = "Rejected connection", + ["#AL#01"] = "Password verification error", + ["#AL#10"] = "Checksum verification error", + ["#ASD#-1"] = "Package structure error", + ["#ASD#0"] = "Incorrect time", + ["#ASD#10"] = "Error getting coordinates", + ["#ASD#11"] = "Error getting speed, course, or altitude", + ["#ASD#12"] = "Error getting the number of satellites", + ["#ASD#13"] = "Checksum verification error", + ["#AD#-1"] = "Package structure error", + ["#AD#0"] = "Incorrect time", + ["#AD#10"] = "Error getting coordinates", + ["#AD#11"] = "Error getting speed, course, or altitude", + ["#AD#12"] = "Error in getting the number of satellites or HDOP", + ["#AD#13"] = "Error getting Inputs or Outputs", + ["#AD#14"] = "Error receiving ADC", + ["#AD#15"] = "Error getting additional parameters", + ["#AD#16"] = "Checksum verification error" + } + + for k, v in pairs(ERROR_CODE) do + if k == resp then + return v + end + end + return "Unknown error" +end + +-- Login Package +local function login(imei, pass) + local L = {} + L[1], L[2], L[3] = "2.0", imei, pass + L[4] = checksum.crc16(table.concat(L, ";") .. ';') + return "#L#" .. table.concat(L, ";") .. "\r\n" +end + +-- Send data to server side +function wialon_ips.sendData(GnssData, serverConfig) + local r, s, e + local DATA_OK = "OK" + local err = {false, DATA_OK} + local wialonData = bigData(GnssData) + + -- Data is missing, there is nothing to send + if string.find(wialonData, "DB2D") then + return {true, "No data to send"} + end + + s, e = tcp:connect(serverConfig.address, serverConfig.port) + if not s then + blackbox.set(GnssData, serverConfig) + tcp:close() + return {true, e} + end + tcp:settimeout(2) + tcp:send(login(serverConfig.login, serverConfig.password) .. '\n') + r, e = tcp:receive() + + if handlErr(r) == DATA_OK then + tcp:send(wialonData) + r, e = tcp:receive() + if handlErr(r) == DATA_OK then + local booferSize, booferData = blackbox.get(serverConfig) + if booferSize > 0 then + tcp:send(booferData .. '\n') + r, e = tcp:receive() + local sentSize = string.gsub(r, "%D", "") + if tonumber(sentSize) and tonumber(sentSize) >= booferSize then + blackbox.clean(serverConfig) + end + end + elseif handlErr(r) ~= DATA_OK then + blackbox.set(GnssData, serverConfig) + end + else + err = {true, handlErr(r)} + blackbox.set(GnssData, serverConfig) + end + + tcp:close() + return err +end + +return wialon_ips \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/proto/wialon_ips_boof.lua b/luci-app-gpoint-main/root/usr/share/gpoint/proto/wialon_ips_boof.lua new file mode 100644 index 000000000..fe556f246 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/proto/wialon_ips_boof.lua @@ -0,0 +1,119 @@ +------------------------------------------------------------- +-- A module for working with the "WIALON IPS" navigation protocol. +-- This module saves navigation data in case of signal loss +-- (if it is not possible to transfer data to the server) +------------------------------------------------------------- +-- Copyright 2021-2022 Vladislav Kadulin +-- Licensed to the GNU General Public License v3.0 + +local json = require("luci.jsonc") +local checksum = require("checksum") + +local wialon_ips_boof = {} + +-- Extended Data package +local function transformBooferData(GnssData, params) + local D = {"NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","NA","","NA","NA"} + if not GnssData.warning.rmc[1] then + D[1], D[2] = GnssData.rmc.date, GnssData.rmc.utc + else + D[1], D[2] = os.date("%d%m%y"), os.date("%H%M%S",os.time(os.date("!*t"))) .. ".00" + end + if not GnssData.warning.gga[1] then + -- Lat2[4], Lon2[6] - NA + D[3], D[5] = GnssData.gga.latitude, GnssData.gga.longitude + D[9], D[10] = GnssData.gga.alt, GnssData.gga.sat + D[11] = GnssData.gga.hdp + elseif not GnssData.warning.gns[1] then + D[3], D[5] = GnssData.gns.latitude, GnssData.gns.longitude + D[9], D[10] = GnssData.gns.alt, GnssData.gns.sat + D[11] = GnssData.gns.hdp + elseif not GnssData.warning.locator[1] then + D[3], D[5] = GnssData.gga.latitude, '0' .. GnssData.gga.longitude + end + if not GnssData.warning.vtg[1] then + D[7], D[8] = GnssData.vtg.speed, GnssData.vtg.course_t + end + if params then + D[16] = string.format("%s:%s:%s", params[1], params[2], params[3]) + else + D[16] = string.format("%s:%s:%s", "boof", '3', "Data from boofer") + end + + return table.concat(D, ";") +end + +-- read GNSS data from file +local function readBoof() + local file = io.open("/usr/share/gpoint/tmp/blackbox.json", 'r') + if not file then return nil end + local bb_data = json.parse(file:read("*a")) + file:close() + return bb_data +end + +-- write GNSS data to file +local function writeBoof(boof) + local file = io.open("/usr/share/gpoint/tmp/blackbox.json", 'w') + file:write(json.stringify(boof)) + file:close() +end + +-- create boofer with data if +local function createBoof(size) + local BLACKBOX = { ["size"]=0,["max"]=tonumber(size),["data"]={} } + writeBoof(BLACKBOX) + return BLACKBOX +end + +-- Package from the black box +local function parseBlackBoxData(data) + local B = "" + for _, gnss_msg in pairs(data) do + B = B .. gnss_msg .. '|' + end + return "#B#" .. B .. checksum.crc16(B) .. "\r\n" +end + +-- get data from the black box +function wialon_ips_boof.get(serverConfig) + if not serverConfig.blackbox.enable then + return -1, nil + end + + local blackbox = readBoof() + if blackbox == nil or tonumber(blackbox.max) ~= tonumber(serverConfig.blackbox.size) then + blackbox = createBoof(serverConfig.blackbox.size) + end + + return blackbox.size, parseBlackBoxData(blackbox.data) +end + +-- send data to the black box +function wialon_ips_boof.set(GnssData, serverConfig) + + if not serverConfig.blackbox.enable then + return -1 + end + + local blackbox = readBoof() + if blackbox == nil or tonumber(blackbox.max) ~= tonumber(serverConfig.blackbox.size) then + blackbox = createBoof(serverConfig.blackbox.size) + end + + if blackbox.size < blackbox.max then + blackbox.size = blackbox.size + 1 + elseif serverConfig.blackbox.cycle then + blackbox.size = 1 + end + + blackbox.data[blackbox.size] = transformBooferData(GnssData) + writeBoof(blackbox) +end + +-- clear the black box +function wialon_ips_boof.clean(serverConfig) + createBoof(serverConfig.blackbox.size) +end + +return wialon_ips_boof \ No newline at end of file diff --git a/luci-app-gpoint-main/root/usr/share/gpoint/tmp/blackbox.json b/luci-app-gpoint-main/root/usr/share/gpoint/tmp/blackbox.json new file mode 100644 index 000000000..bcf57a157 --- /dev/null +++ b/luci-app-gpoint-main/root/usr/share/gpoint/tmp/blackbox.json @@ -0,0 +1 @@ +{"max":1000,"data":[],"size":0} \ No newline at end of file diff --git a/luci-app-gpoint-main/test/chechsum_test.lua b/luci-app-gpoint-main/test/chechsum_test.lua new file mode 100644 index 000000000..9356457e1 --- /dev/null +++ b/luci-app-gpoint-main/test/chechsum_test.lua @@ -0,0 +1,143 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + +local checksum = require("checksum") +local lu = require('luaunit') + +local goodGNSSSdata = { + "$GPRMC,113702.568,V,4154.931,N,08002.497,W,95.5,0.02,220721,,E*4E", + "$GPGGA,113703.568,4154.931,N,08002.497,W,0,00,,,M,,M,,*52", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.1,0.8,0.6*3F", + "$GPRMC,113705.568,V,4154.933,N,08002.497,W,86.0,-0.05,220721,,E*66", + "$GPGGA,113706.568,4154.933,N,08002.497,W,0,00,0.8,,M,,M,,*73", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.0,0.4*36", + "$GPRMC,113708.568,V,4154.935,N,08002.498,W,55.1,-0.10,220721,,E*69", + "$GPGGA,113709.568,4154.935,N,08002.498,W,0,00,0.0,,M,,M,,*7D", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.2,0.8*3C", + "$GPRMC,113711.568,V,4154.937,N,08002.498,W,95.0,-0.10,220721,,E*6E", + "$GPGGA,113712.568,4154.937,N,08002.498,W,0,00,0.2,,M,,M,,*77", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.7,0.6,0.3*32", + "$GPRMC,113714.568,V,4154.939,N,08002.498,W,28.0,-0.07,220721,,E*65", + "$GPGGA,113715.568,4154.939,N,08002.498,W,0,00,0.6,,M,,M,,*7A", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,1.0,0.5*34", + "$GPRMC,113717.568,V,4154.940,N,08002.498,W,30.1,0.03,220721,,E*49", + "$GPGGA,113718.568,4154.940,N,08002.498,W,0,00,1.0,,M,,M,,*7E", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.5,0.1,0.2*36", + "$GPRMC,113720.568,V,4154.942,N,08002.498,W,0.2,-0.02,220721,,E*53", + "$GPGGA,113721.568,4154.942,N,08002.498,W,0,00,0.1,,M,,M,,*76", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.5,0.7*30", + "$GPRMC,113723.568,V,4154.944,N,08002.498,W,53.6,0.05,220721,,E*4E", + "$GPGGA,113724.568,4154.944,N,08002.498,W,0,00,0.5,,M,,M,,*71", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,0.6,0.1*37", + "$GPRMC,113726.568,V,4154.946,N,08002.498,W,76.6,0.04,220721,,E*4F", + "$GPGGA,113727.568,4154.946,N,08002.498,W,0,00,0.6,,M,,M,,*73", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.2,0.3,0.7*37", + "$GPRMC,113729.568,V,4154.948,N,08002.497,W,30.9,0.12,220721,,E*4B", + "$GPGGA,113730.568,4154.948,N,08002.497,W,0,00,0.3,,M,,M,,*71", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.2,0.2*34", + "$GPRMC,113732.568,V,4154.949,N,08002.497,W,47.2,0.20,220721,,E*4A", + "$GPGGA,113733.568,4154.949,N,08002.497,W,0,00,0.2,,M,,M,,*72", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.3,0.8,0.2*39", + "$GPRMC,113735.568,V,4154.951,N,08002.496,W,31.1,0.13,220721,,E*47", + "$GPGGA,113736.568,4154.951,N,08002.496,W,0,00,0.8,,M,,M,,*75", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.6,0.1*35", + "$GPRMC,113738.568,V,4154.953,N,08002.496,W,58.2,0.10,220721,,E*47", + "$GPGGA,113739.568,4154.953,N,08002.496,W,0,00,0.6,,M,,M,,*76", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.6,0.9,0.7*39", + "$GPRMC,113741.568,V,4154.955,N,08002.496,W,88.3,0.03,220721,,E*41", + "$GPGGA,113742.568,4154.955,N,08002.496,W,0,00,0.9,,M,,M,,*73", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.8,0.8,0.1*30", + "$GPRMC,113744.568,V,4154.956,N,08002.496,W,89.3,0.10,220721,,E*44", + "$GPGGA,113745.568,4154.956,N,08002.496,W,0,00,0.8,,M,,M,,*76", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.9,0.2,1.0*3B", + "$GPRMC,113747.568,V,4154.958,N,08002.495,W,99.1,0.14,220721,,E*4D", + "$GPGGA,113748.568,4154.958,N,08002.495,W,0,00,0.2,,M,,M,,*7C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.1,0.2*35", + "$GPRMC,113750.568,V,4154.960,N,08002.495,W,84.0,0.19,220721,,E*40", + "$GPGGA,113751.568,4154.960,N,08002.495,W,0,00,0.1,,M,,M,,*7C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.3,1.0,0.5*36", + "$GPRMC,113753.568,V,4154.962,N,08002.495,W,24.0,0.13,220721,,E*41", + "$GPGGA,113754.568,4154.962,N,08002.495,W,0,00,1.0,,M,,M,,*7B", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.8,0.9*37", + "$GPRMC,113756.568,V,4154.963,N,08002.494,W,27.8,0.03,220721,,E*4E", + "$GPGGA,113757.568,4154.963,N,08002.494,W,0,00,0.8,,M,,M,,*71", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.4,0.7*37" +} + +local badGNSSSdata = { + "$GPRMC,113702.568,V,4154.931,N,08002.497,W,95.5,0.02,220721,,E*48", + "$GPGGA,113703.568,4154.931,N,08002.497,W,0,00,,,M,,M,,*54", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.1,0.8,0.6*33", + "$GPRMC,113720.568,V,4154.942,N,08002.498,W,0.2,-0.02,220721,,E*5F", + "$GPGGA,113721.568,4154.942,N,08002.498,W,0,00,0.1,,M,,M,,*82", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.5,0.7*35", + "$GPRMC,113723.568,V,4154.944,N,08002.498,W,53.6,0.05,220721,,E*5A", + "$GPGGA,113724.568,4154.944,N,08002.498,W,0,00,0.5,,M,,M,,*12", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,0.6,0.1*35", + "$GPRMC,113726.568,V,4154.946,N,08002.498,W,76.6,0.04,220721,,E*9E", + "$GPGGA,113727.568,4154.946,N,08002.498,W,0,00,0.6,,M,,M,,*94", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.2,0.3,0.7*32", + "$GPRMC,113729.568,V,4154.948,N,08002.497,W,30.9,0.12,220721,,E*9C", + "$GPGGA,113730.568,4154.948,N,08002.497,W,0,00,0.3,,M,,M,,*79", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.2,0.2*39", + "$GPRMC,113705.568,V,4154.933,N,08002.497,W,86.0,-0.05,220721,,E*90", + "$GPGGA,113706.568,4154.933,N,08002.497,W,0,00,0.8,,M,,M,,*42", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.0,0.4*43", + "$GPRMC,113708.568,V,4154.935,N,08002.498,W,55.1,-0.10,220721,,E*44", + "$GPGGA,113709.568,4154.935,N,08002.498,W,0,00,0.0,,M,,M,,*4A", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.2,0.8*4D", + "$GPRMC,113711.568,V,4154.937,N,08002.498,W,95.0,-0.10,220721,,E*44", + "$GPGGA,113712.568,4154.937,N,08002.498,W,0,00,0.2,,M,,M,,*44", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.7,0.6,0.3*4D", + "$GPRMC,113714.568,V,4154.939,N,08002.498,W,28.0,-0.07,220721,,E*4D", + "$GPGGA,113715.568,4154.939,N,08002.498,W,0,00,0.6,,M,,M,,*4D", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,1.0,0.5*24", + "$GPRMC,113717.568,V,4154.940,N,08002.498,W,30.1,0.03,220721,,E*59", + "$GPGGA,113718.568,4154.940,N,08002.498,W,0,00,1.0,,M,,M,,*4D", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.5,0.1,0.2*39", + "$GPGGA,113736.568,4154.951,N,08002.496,W,0,00,0.8,,M,,M,,*79", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.6,0.1*39", + "$GPRMC,113738.568,V,4154.953,N,08002.496,W,58.2,0.10,220721,,E*67", + "$GPGGA,113739.568,4154.953,N,08002.496,W,0,00,0.6,,M,,M,,*79", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.6,0.9,0.7*35", + "$GPRMC,113741.568,V,4154.955,N,08002.496,W,88.3,0.03,220721,,E*31", + "$GPGGA,113742.568,4154.955,N,08002.496,W,0,00,0.9,,M,,M,,*33", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.8,0.8,0.1*34", + "$GPRMC,113744.568,V,4154.956,N,08002.496,W,89.3,0.10,220721,,E*34", + "$GPGGA,113745.568,4154.956,N,08002.496,W,0,00,0.8,,M,,M,,*75", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.9,0.2,1.0*4B", + "$GPRMC,113747.568,V,4154.958,N,08002.495,W,99.1,0.14,220721,,E*5D", + "$GPGGA,113748.568,4154.958,N,08002.495,W,0,00,0.2,,M,,M,,*5C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.1,0.2*55", + "$GPRMC,113750.568,V,4154.960,N,08002.495,W,84.0,0.19,220721,,E*50", + "$GPGGA,113751.568,4154.960,N,08002.495,W,0,00,0.1,,M,,M,,*5C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.3,1.0,0.5*35", + "$GPRMC,113753.568,V,4154.962,N,08002.495,W,24.0,0.13,220721,,E*51", + "$GPGGA,113754.568,4154.962,N,08002.495,W,0,00,1.0,,M,,M,,*5B", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.8,0.9*35", + "$GPRMC,113756.568,V,4154.963,N,08002.494,W,27.8,0.03,220721,,E*5E", + "$GPGGA,113757.568,4154.963,N,08002.494,W,0,00,0.8,,M,,M,,*51", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.4,0.7*57" +} + +function testCRC8() + for i = 1, #goodGNSSSdata do + lu.assertEquals(checksum.crc8(goodGNSSSdata[i]), true) + end + for i = 1, #badGNSSSdata do + lu.assertEquals(checksum.crc8(badGNSSSdata[i]), false) + end +end +local a = checksum.crc16("$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.4,0.7*57") + +function testCRC16() + lu.assertEquals(checksum.crc16(goodGNSSSdata[1]), "E50C") + lu.assertEquals(checksum.crc16(goodGNSSSdata[2]), "CCD5") + lu.assertEquals(checksum.crc16(goodGNSSSdata[3]), "852C") + lu.assertEquals(checksum.crc16(goodGNSSSdata[4]), "EDB5") + lu.assertEquals(checksum.crc16(goodGNSSSdata[5]), "5DF0") + lu.assertEquals(checksum.crc16(goodGNSSSdata[6]), "7B29") + lu.assertEquals(checksum.crc16(goodGNSSSdata[7]), "7CC") +end + +os.exit(lu.LuaUnit.run()) diff --git a/luci-app-gpoint-main/test/gps_test.lua b/luci-app-gpoint-main/test/gps_test.lua new file mode 100644 index 000000000..ac6d2c237 --- /dev/null +++ b/luci-app-gpoint-main/test/gps_test.lua @@ -0,0 +1,96 @@ +local gps = require("gps_lib") +--local kalman = require("kalman_lib") + +function read_lat_lon() + local file = io.open("/www/kalman_lua/matrix_c_test/testdata/gps_example_1", 'r') + + while file:read() do + local lat, lon = file:read("*n", "*n") + if lat and lon then + return lat, lon + end + end + + file:close() + print("read_lat_lon - OK") +end + +function test_read_lat_long() + local lat, lon = read_lat_lon() + assert(math.abs(lat - 39.315828) < 0.000001) + assert(math.abs(lon - -120.167838) < 0.000001) + for i = 1, 131 do + local lat, lon = read_lat_lon() + end + print("test_read_lat_long - OK") +end + +function test_bearing_north() + local kalman = gps.create_velocity2d(1.0) + for i = 1, 100 do + kalman = gps.update_velocity2d(kalman, i * 0.0001, 0.0, 1.0) + end + + local bearing = gps.get_bearing(kalman) + assert(math.abs(bearing - 0.0) < 0.01) + + local dlat, dlon = gps.get_velocity(kalman) + assert(math.abs(dlat - 0.0001) < 0.00001) + assert(math.abs(dlon) < 0.00001) + print("test_bearing_north - OK") +end + +function test_bearing_east() + local kalman = gps.create_velocity2d(1.0) + for i = 1, 100 do + kalman = gps.update_velocity2d(kalman, 0.0, i * 0.0001, 1.0) + end + + local bearing = gps.get_bearing(kalman) + assert(math.abs(bearing - 90.0) < 0.01) + --At this rate, it takes 10,000 timesteps to travel one longitude + --unit, and thus 3,600,000 timesteps to travel the circumference of + --the earth. Let's say one timestep is a second, so it takes + --3,600,000 seconds, which is 60,000 minutes, which is 1,000 + --hours. Since the earth is about 25000 miles around, this means we + --are traveling at about 25 miles per hour. + local mph = gps.get_mph(kalman) + assert(math.abs(mph - 25.0) < 2.0) + print("test_bearing_east - OK") +end + +function test_bearing_south() + local kalman = gps.create_velocity2d(1.0) + for i = 1, 100 do + kalman = gps.update_velocity2d(kalman, i * -0.0001, 0.0, 1.0) + end + + local bearing = gps.get_bearing(kalman) + assert(math.abs(bearing - 180.0) < 0.01) + print("test_bearing_south - OK") +end + +function test_bearing_west() + local kalman = gps.create_velocity2d(1.0) + for i = 1, 100 do + kalman = gps.update_velocity2d(kalman, 0.0, i * -0.0001, 1.0) + end + + local bearing = gps.get_bearing(kalman) + assert(math.abs(bearing - 270.0) < 0.01) + print("test_bearing_west - OK") +end + +function test_calculate_mph() + local mph = gps.calculate_mph(39.315842, -120.167107, -0.000031, 0.000003); + assert(math.abs(mph - 7.74) < 0.01); + print("test_calculate_mph - OK") +end + +-- test start +test_read_lat_long() +test_bearing_north() +test_bearing_east() +test_bearing_south() +test_bearing_west() +test_calculate_mph() \ No newline at end of file diff --git a/luci-app-gpoint-main/test/kalman_test.lua b/luci-app-gpoint-main/test/kalman_test.lua new file mode 100644 index 000000000..a2b5ab674 --- /dev/null +++ b/luci-app-gpoint-main/test/kalman_test.lua @@ -0,0 +1,34 @@ +local kalman = require("kalman_lib") +local matrix = require("matrix_lib") + +-- Test the example of a train moving along a 1-d track +function test_train() + local k = kalman.create(2, 1) + -- The train state is a 2d vector containing position and velocity. + -- Velocity is measured in position units per timestep units. + k.state_transition = matrix.set(k.state_transition, 1.0, 1.0, + 0.0, 1.0) + -- We only observe position + k.observation_model = matrix.set(k.observation_model, 1.0, 0.0) + -- The covariance matrices are blind guesses + k.process_noise_covariance = matrix.set_identity(k.process_noise_covariance) + k.observation_noise_covariance = matrix.set_identity(k.observation_noise_covariance) + -- Our knowledge of the start position is incorrect and unconfident + local deviation = 1000.0 + k.state_estimate = matrix.set(k.state_estimate, 10 * deviation) + k.estimate_covariance = matrix.set_identity(k.estimate_covariance) + k.estimate_covariance = matrix.scale(k.estimate_covariance, deviation * deviation) + + for i = 1, 10 do + k.observation = matrix.set(k.observation, i) + k = kalman.update(k) + end + + + print("estimated position: " .. tostring(k.state_estimate[1][1])) + print("estimated position: " .. tostring(k.state_estimate[2][1])) + +end + +test_train() +print("OK") \ No newline at end of file diff --git a/luci-app-gpoint-main/test/luaunit.lua b/luci-app-gpoint-main/test/luaunit.lua new file mode 100644 index 000000000..7ac4b0e89 --- /dev/null +++ b/luci-app-gpoint-main/test/luaunit.lua @@ -0,0 +1,3452 @@ +--[[ + luaunit.lua + +Description: A unit testing framework +Homepage: https://github.com/bluebird75/luaunit +Development by Philippe Fremy +Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) +License: BSD License, see LICENSE.txt +]]-- + +require("math") +local M={} + +-- private exported functions (for testing) +M.private = {} + +M.VERSION='3.4' +M._VERSION=M.VERSION -- For LuaUnit v2 compatibility + +-- a version which distinguish between regular Lua and LuaJit +M._LUAVERSION = (jit and jit.version) or _VERSION + +--[[ Some people like assertEquals( actual, expected ) and some people prefer +assertEquals( expected, actual ). +]]-- +M.ORDER_ACTUAL_EXPECTED = true +M.PRINT_TABLE_REF_IN_ERROR_MSG = false +M.LINE_LENGTH = 80 +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items + +-- this setting allow to remove entries from the stack-trace, for +-- example to hide a call to a framework which would be calling luaunit +M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 0 + +--[[ EPS is meant to help with Lua's floating point math in simple corner +cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers +with rational binary representation) if the user doesn't provide some explicit +error margin. + +The default margin used by almostEquals() in such cases is EPS; and since +Lua may be compiled with different numeric precisions (single vs. double), we +try to select a useful default for it dynamically. Note: If the initial value +is not acceptable, it can be changed by the user to better suit specific needs. + +See also: https://en.wikipedia.org/wiki/Machine_epsilon +]] +M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 +if math.abs(1.1 - 1 - 0.1) > M.EPS then + -- rounding error is above EPS, assume single precision + M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 +end + +-- set this to false to debug luaunit +local STRIP_LUAUNIT_FROM_STACKTRACE = true + +M.VERBOSITY_DEFAULT = 10 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 +M.VERBOSITY_VERBOSE = 20 +M.DEFAULT_DEEP_ANALYSIS = nil +M.FORCE_DEEP_ANALYSIS = true +M.DISABLE_DEEP_ANALYSIS = false + +-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values +-- EXPORT_ASSERT_TO_GLOBALS = true + +-- we need to keep a copy of the script args before it is overriden +local cmdline_argv = rawget(_G, "arg") + +M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests +M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early +M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests + + + +M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] +Options: + -h, --help: Print this help + --version: Print version information + -v, --verbose: Increase verbosity + -q, --quiet: Set verbosity to minimum + -e, --error: Stop on first error + -f, --failure: Stop on first failure or error + -s, --shuffle: Shuffle tests before running them + -o, --output OUTPUT: Set output type to OUTPUT + Possible values: text, tap, junit, nil + -n, --name NAME: For junit only, mandatory name of xml file + -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT + -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN + May be repeated to include several patterns + Make sure you escape magic chars like +? with % + -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN + May be repeated to exclude several patterns + Make sure you escape magic chars like +? with % + testname1, testname2, ... : tests to run in the form of testFunction, + TestClass or TestClass.testMethod + +You may also control LuaUnit options with the following environment variables: +* LUAUNIT_OUTPUT: same as --output +* LUAUNIT_JUNIT_FNAME: same as --name ]] + +---------------------------------------------------------------- +-- +-- general utility functions +-- +---------------------------------------------------------------- + +--[[ Note on catching exit + +I have seen the case where running a big suite of test cases and one of them would +perform a os.exit(0), making the outside world think that the full test suite was executed +successfully. + +This is an attempt to mitigate this problem: we override os.exit() to now let a test +exit the framework while we are running. When we are not running, it behaves normally. +]] + +M.oldOsExit = os.exit +os.exit = function(...) + if M.LuaUnit and #M.LuaUnit.instances ~= 0 then + local msg = [[You are trying to exit but there is still a running instance of LuaUnit. +LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests. + +To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded): + + lu.unregisterCurrentSuite() + +]] + M.private.error_fmt(2, msg) + end + M.oldOsExit(...) +end + +local function pcall_or_abort(func, ...) + -- unpack is a global function for Lua 5.1, otherwise use table.unpack + local unpack = rawget(_G, "unpack") or table.unpack + local result = {pcall(func, ...)} + if not result[1] then + -- an error occurred + print(result[2]) -- error message + print() + print(M.USAGE) + os.exit(-1) + end + return unpack(result, 2) +end + +local crossTypeOrdering = { + number = 1, boolean = 2, string = 3, table = 4, other = 5 +} +local crossTypeComparison = { + number = function(a, b) return a < b end, + string = function(a, b) return a < b end, + other = function(a, b) return tostring(a) < tostring(b) end, +} + +local function crossTypeSort(a, b) + local type_a, type_b = type(a), type(b) + if type_a == type_b then + local func = crossTypeComparison[type_a] or crossTypeComparison.other + return func(a, b) + end + type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other + type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other + return type_a < type_b +end + +local function __genSortedIndex( t ) + -- Returns a sequence consisting of t's keys, sorted. + local sortedIndex = {} + + for key,_ in pairs(t) do + table.insert(sortedIndex, key) + end + + table.sort(sortedIndex, crossTypeSort) + return sortedIndex +end +M.private.__genSortedIndex = __genSortedIndex + +local function sortedNext(state, control) + -- Equivalent of the next() function of table iteration, but returns the + -- keys in sorted order (see __genSortedIndex and crossTypeSort). + -- The state is a temporary variable during iteration and contains the + -- sorted key table (state.sortedIdx). It also stores the last index (into + -- the keys) used by the iteration, to find the next one quickly. + local key + + --print("sortedNext: control = "..tostring(control) ) + if control == nil then + -- start of iteration + state.count = #state.sortedIdx + state.lastIdx = 1 + key = state.sortedIdx[1] + return key, state.t[key] + end + + -- normally, we expect the control variable to match the last key used + if control ~= state.sortedIdx[state.lastIdx] then + -- strange, we have to find the next value by ourselves + -- the key table is sorted in crossTypeSort() order! -> use bisection + local lower, upper = 1, state.count + repeat + state.lastIdx = math.modf((lower + upper) / 2) + key = state.sortedIdx[state.lastIdx] + if key == control then + break -- key found (and thus prev index) + end + if crossTypeSort(key, control) then + -- key < control, continue search "right" (towards upper bound) + lower = state.lastIdx + 1 + else + -- key > control, continue search "left" (towards lower bound) + upper = state.lastIdx - 1 + end + until lower > upper + if lower > upper then -- only true if the key wasn't found, ... + state.lastIdx = state.count -- ... so ensure no match in code below + end + end + + -- proceed by retrieving the next value (or nil) from the sorted keys + state.lastIdx = state.lastIdx + 1 + key = state.sortedIdx[state.lastIdx] + if key then + return key, state.t[key] + end + + -- getting here means returning `nil`, which will end the iteration +end + +local function sortedPairs(tbl) + -- Equivalent of the pairs() function on tables. Allows to iterate in + -- sorted order. As required by "generic for" loops, this will return the + -- iterator (function), an "invariant state", and the initial control value. + -- (see http://www.lua.org/pil/7.2.html) + return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil +end +M.private.sortedPairs = sortedPairs + +-- seed the random with a strongly varying seed +math.randomseed(math.floor((os.clock()*1E11) / 100)) -- add /100 OpenWrt some bug + +local function randomizeTable( t ) + -- randomize the item orders of the table t + for i = #t, 2, -1 do + local j = math.random(i) + if i ~= j then + t[i], t[j] = t[j], t[i] + end + end +end +M.private.randomizeTable = randomizeTable + +local function strsplit(delimiter, text) +-- Split text into a list consisting of the strings in text, separated +-- by strings matching delimiter (which may _NOT_ be a pattern). +-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + if delimiter == "" or delimiter == nil then -- this would result in endless loops + error("delimiter is nil or empty string!") + end + if text == nil then + return nil + end + + local list, pos, first, last = {}, 1 + while true do + first, last = text:find(delimiter, pos, true) + if first then -- found? + table.insert(list, text:sub(pos, first - 1)) + pos = last + 1 + else + table.insert(list, text:sub(pos)) + break + end + end + return list +end +M.private.strsplit = strsplit + +local function hasNewLine( s ) + -- return true if s has a newline + return (string.find(s, '\n', 1, true) ~= nil) +end +M.private.hasNewLine = hasNewLine + +local function prefixString( prefix, s ) + -- Prefix all the lines of s with prefix + return prefix .. string.gsub(s, '\n', '\n' .. prefix) +end +M.private.prefixString = prefixString + +local function strMatch(s, pattern, start, final ) + -- return true if s matches completely the pattern from index start to index end + -- return false in every other cases + -- if start is nil, matches from the beginning of the string + -- if final is nil, matches to the end of the string + start = start or 1 + final = final or string.len(s) + + local foundStart, foundEnd = string.find(s, pattern, start, false) + return foundStart == start and foundEnd == final +end +M.private.strMatch = strMatch + +local function patternFilter(patterns, expr) + -- Run `expr` through the inclusion and exclusion rules defined in patterns + -- and return true if expr shall be included, false for excluded. + -- Inclusion pattern are defined as normal patterns, exclusions + -- patterns start with `!` and are followed by a normal pattern + + -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT + -- default: true if no explicit "include" is found, set to false otherwise + local default, result = true, nil + + if patterns ~= nil then + for _, pattern in ipairs(patterns) do + local exclude = pattern:sub(1,1) == '!' + if exclude then + pattern = pattern:sub(2) + else + -- at least one include pattern specified, a match is required + default = false + end + -- print('pattern: ',pattern) + -- print('exclude: ',exclude) + -- print('default: ',default) + + if string.find(expr, pattern) then + -- set result to false when excluding, true otherwise + result = not exclude + end + end + end + + if result ~= nil then + return result + end + return default +end +M.private.patternFilter = patternFilter + +local function xmlEscape( s ) + -- Return s escaped for XML attributes + -- escapes table: + -- " " + -- ' ' + -- < < + -- > > + -- & & + + return string.gsub( s, '.', { + ['&'] = "&", + ['"'] = """, + ["'"] = "'", + ['<'] = "<", + ['>'] = ">", + } ) +end +M.private.xmlEscape = xmlEscape + +local function xmlCDataEscape( s ) + -- Return s escaped for CData section, escapes: "]]>" + return string.gsub( s, ']]>', ']]>' ) +end +M.private.xmlCDataEscape = xmlCDataEscape + + +local function lstrip( s ) + --[[Return s with all leading white spaces and tabs removed]] + local idx = 0 + while idx < s:len() do + idx = idx + 1 + local c = s:sub(idx,idx) + if c ~= ' ' and c ~= '\t' then + break + end + end + return s:sub(idx) +end +M.private.lstrip = lstrip + +local function extractFileLineInfo( s ) + --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" + + Return the "file.lua:linenb" information + ]] + local s2 = lstrip(s) + local firstColon = s2:find(':', 1, true) + if firstColon == nil then + -- string is not in the format file:line: + return s + end + local secondColon = s2:find(':', firstColon+1, true) + if secondColon == nil then + -- string is not in the format file:line: + return s + end + + return s2:sub(1, secondColon-1) +end +M.private.extractFileLineInfo = extractFileLineInfo + + +local function stripLuaunitTrace2( stackTrace, errMsg ) + --[[ + -- Example of a traceback: + < + [C]: in function 'xpcall' + ./luaunit.lua:1449: in function 'protectedCall' + ./luaunit.lua:1508: in function 'execOneFunction' + ./luaunit.lua:1596: in function 'runSuiteByInstances' + ./luaunit.lua:1660: in function 'runSuiteByNames' + ./luaunit.lua:1736: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + Other example: + < + [C]: in function 'xpcall' + ./luaunit.lua:1517: in function 'protectedCall' + ./luaunit.lua:1578: in function 'execOneFunction' + ./luaunit.lua:1677: in function 'runSuiteByInstances' + ./luaunit.lua:1730: in function 'runSuiteByNames' + ./luaunit.lua:1806: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + < + [C]: in function 'xpcall' + luaunit2/luaunit.lua:1532: in function 'protectedCall' + luaunit2/luaunit.lua:1591: in function 'execOneFunction' + luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' + luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' + luaunit2/luaunit.lua:1819: in function 'runSuite' + luaunit2/example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + + -- first line is "stack traceback": KEEP + -- next line may be luaunit line: REMOVE + -- next lines are call in the program under testOk: REMOVE + -- next lines are calls from luaunit to call the program under test: KEEP + + -- Strategy: + -- keep first line + -- remove lines that are part of luaunit + -- kepp lines until we hit a luaunit line + + The strategy for stripping is: + * keep first line "stack traceback:" + * part1: + * analyse all lines of the stack from bottom to top of the stack (first line to last line) + * extract the "file:line:" part of the line + * compare it with the "file:line" part of the error message + * if it does not match strip the line + * if it matches, keep the line and move to part 2 + * part2: + * anything NOT starting with luaunit.lua is the interesting part of the stack trace + * anything starting again with luaunit.lua is part of the test launcher and should be stripped out + ]] + + local function isLuaunitInternalLine( s ) + -- return true if line of stack trace comes from inside luaunit + return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil + end + + -- print( '<<'..stackTrace..'>>' ) + + local t = strsplit( '\n', stackTrace ) + -- print( prettystr(t) ) + + local idx = 2 + + local errMsgFileLine = extractFileLineInfo(errMsg) + -- print('emfi="'..errMsgFileLine..'"') + + -- remove lines that are still part of luaunit + while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do + -- print('Removing : '..t[idx] ) + table.remove(t, idx) + end + + -- keep lines until we hit luaunit again + while t[idx] and (not isLuaunitInternalLine(t[idx])) do + -- print('Keeping : '..t[idx] ) + idx = idx + 1 + end + + -- remove remaining luaunit lines + while t[idx] do + -- print('Removing2 : '..t[idx] ) + table.remove(t, idx) + end + + -- print( prettystr(t) ) + return table.concat( t, '\n') + +end +M.private.stripLuaunitTrace2 = stripLuaunitTrace2 + + +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) + local type_v = type(v) + if "string" == type_v then + -- use clever delimiters according to content: + -- enclose with single quotes if string contains ", but no ' + if v:find('"', 1, true) and not v:find("'", 1, true) then + return "'" .. v .. "'" + end + -- use double quotes otherwise, escape embedded " + return '"' .. v:gsub('"', '\\"') .. '"' + + elseif "table" == type_v then + --if v.__class__ then + -- return string.gsub( tostring(v), 'table', v.__class__ ) + --end + return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) + + elseif "number" == type_v then + -- eliminate differences in formatting between various Lua versions + if v ~= v then + return "#NaN" -- "not a number" + end + if v == math.huge then + return "#Inf" -- "infinite" + end + if v == -math.huge then + return "-#Inf" + end + if _VERSION == "Lua 5.3" then + local i = math.tointeger(v) + if i then + return tostring(i) + end + end + end + + return tostring(v) +end + +local function prettystr( v ) + --[[ Pretty string conversion, to display the full content of a variable of any type. + + * string are enclosed with " by default, or with ' if string contains a " + * tables are expanded to show their full content, with indentation in case of nested tables + ]]-- + local cycleDetectTable = {} + local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) + if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then + -- some table contain recursive references, + -- so we must recompute the value by including all table references + -- else the result looks like crap + cycleDetectTable = {} + s = prettystr_sub(v, 1, true, cycleDetectTable) + end + return s +end +M.prettystr = prettystr + +function M.adjust_err_msg_with_iter( err_msg, iter_msg ) + --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, + add the iteration message if any and return the result. + + err_msg: string, error message captured with pcall + iter_msg: a string describing the current iteration ("iteration N") or nil + if there is no iteration in this test. + + Returns: (new_err_msg, test_status) + new_err_msg: string, adjusted error message, or nil in case of success + test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information + contained in the error message. + ]] + if iter_msg then + iter_msg = iter_msg..', ' + else + iter_msg = '' + end + + local RE_FILE_LINE = '.*:%d+: ' + + -- error message is not necessarily a string, + -- so convert the value to string with prettystr() + if type( err_msg ) ~= 'string' then + err_msg = prettystr( err_msg ) + end + + if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then + -- test finished early with success() + return nil, M.NodeStatus.SUCCESS + end + + if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.SKIP + end + + if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.FAIL + end + + + + -- print("error detected") + -- regular error, not a failure + if iter_msg then + local match + -- "./test\\test_luaunit.lua:2241: some error msg + match = err_msg:match( '(.*:%d+: ).*' ) + if match then + err_msg = err_msg:gsub( match, match .. iter_msg ) + else + -- no file:line: infromation, just add the iteration info at the beginning of the line + err_msg = iter_msg .. err_msg + end + end + return err_msg, M.NodeStatus.ERROR +end + +local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) + --[[ + Prepares a nice error message when comparing tables, performing a deeper + analysis. + + Arguments: + * table_a, table_b: tables to be compared + * doDeepAnalysis: + M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries + M.FORCE_DEEP_ANALYSIS : always perform deep analysis + M.DISABLE_DEEP_ANALYSIS: never perform deep analysis + * margin: supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- check if table_a & table_b are suitable for deep analysis + if type(table_a) ~= 'table' or type(table_b) ~= 'table' then + return false + end + + if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then + return false + end + + local len_a, len_b, isPureList = #table_a, #table_b, true + + for k1, v1 in pairs(table_a) do + if type(k1) ~= 'number' or k1 > len_a then + -- this table a mapping + isPureList = false + break + end + end + + if isPureList then + for k2, v2 in pairs(table_b) do + if type(k2) ~= 'number' or k2 > len_b then + -- this table a mapping + isPureList = false + break + end + end + end + + if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then + if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then + return false + end + end + + if isPureList then + return M.private.mismatchFormattingPureList( table_a, table_b, margin ) + else + -- only work on mapping for the moment + -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + return false + end +end +M.private.tryMismatchFormatting = tryMismatchFormatting + +local function getTaTbDescr() + if not M.ORDER_ACTUAL_EXPECTED then + return 'expected', 'actual' + end + return 'actual', 'expected' +end + +local function extendWithStrFmt( res, ... ) + table.insert( res, string.format( ... ) ) +end + +local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables which are not pure lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- disable for the moment + --[[ + local result = {} + local descrTa, descrTb = getTaTbDescr() + + local keysCommon = {} + local keysOnlyTa = {} + local keysOnlyTb = {} + local keysDiffTaTb = {} + + local k, v + + for k,v in pairs( table_a ) do + if is_equal( v, table_b[k] ) then + table.insert( keysCommon, k ) + else + if table_b[k] == nil then + table.insert( keysOnlyTa, k ) + else + table.insert( keysDiffTaTb, k ) + end + end + end + + for k,v in pairs( table_b ) do + if not is_equal( v, table_a[k] ) and table_a[k] == nil then + table.insert( keysOnlyTb, k ) + end + end + + local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa + local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb + local limited_display = (len_a < 5 or len_b < 5) + + if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then + return false + end + + if not limited_display then + if len_a == len_b then + extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) + else + extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) + end + + if #keysCommon == 0 and #keysDiffTaTb == 0 then + table.insert( result, 'Table A and B have no keys in common, they are totally different') + else + local s_other = 'other ' + if #keysCommon then + extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) + else + table.insert( result, 'Table A and B have no identical items' ) + s_other = '' + end + + if #keysDiffTaTb ~= 0 then + result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) + else + result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) + end + end + + extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) + end + + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr(k) + end + + if #keysDiffTaTb ~= 0 then + table.insert( result, 'Items differing in A and B:') + for k,v in sortedPairs( keysDiffTaTb ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysOnlyTa ~= 0 then + table.insert( result, 'Items only in table A:' ) + for k,v in sortedPairs( keysOnlyTa ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + if #keysOnlyTb ~= 0 then + table.insert( result, 'Items only in table B:' ) + for k,v in sortedPairs( keysOnlyTb ) do + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysCommon ~= 0 then + table.insert( result, 'Items common to A and B:') + for k,v in sortedPairs( keysCommon ) do + extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + return true, table.concat( result, '\n') + ]] +end +M.private.mismatchFormattingMapping = mismatchFormattingMapping + +local function mismatchFormattingPureList( table_a, table_b, margin ) + --[[ + Prepares a nice error message when comparing tables which are lists, performing a deeper + analysis. + + margin is supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + local result, descrTa, descrTb = {}, getTaTbDescr() + + local len_a, len_b, refa, refb = #table_a, #table_b, '', '' + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) + end + local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) + local deltalv = longest - shortest + + local commonUntil = shortest + for i = 1, shortest do + if not M.private.is_table_equals(table_a[i], table_b[i], margin) then + commonUntil = i - 1 + break + end + end + + local commonBackTo = shortest - 1 + for i = 0, shortest - 1 do + if not M.private.is_table_equals(table_a[len_a-i], table_b[len_b-i], margin) then + commonBackTo = i - 1 + break + end + end + + + table.insert( result, 'List difference analysis:' ) + if len_a == len_b then + -- TODO: handle expected/actual naming + extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) + else + extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) + end + + extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) + if commonBackTo >= 0 then + if deltalv > 0 then + extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) + else + extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) + end + end + + local function insertABValue(ai, bi) + bi = bi or ai + if M.private.is_table_equals( table_a[ai], table_b[bi], margin) then + return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) + else + extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) + extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) + end + end + + -- common parts to list A & B, at the beginning + if commonUntil > 0 then + table.insert( result, '* Common parts:' ) + for i = 1, commonUntil do + insertABValue( i ) + end + end + + -- diffing parts to list A & B + if commonUntil < shortest - commonBackTo - 1 then + table.insert( result, '* Differing parts:' ) + for i = commonUntil + 1, shortest - commonBackTo - 1 do + insertABValue( i ) + end + end + + -- display indexes of one list, with no match on other list + if shortest - commonBackTo <= longest - commonBackTo - 1 then + table.insert( result, '* Present only in one list:' ) + for i = shortest - commonBackTo, longest - commonBackTo - 1 do + if len_a > len_b then + extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) + -- table.insert( result, '+ (no matching B index)') + else + -- table.insert( result, '- no matching A index') + extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) + end + end + end + + -- common parts to list A & B, at the end + if commonBackTo >= 0 then + table.insert( result, '* Common parts at the end of the lists' ) + for i = longest - commonBackTo, longest do + if len_a > len_b then + insertABValue( i, i-deltalv ) + else + insertABValue( i-deltalv, i ) + end + end + end + + return true, table.concat( result, '\n') +end +M.private.mismatchFormattingPureList = mismatchFormattingPureList + +local function prettystrPairs(value1, value2, suffix_a, suffix_b) + --[[ + This function helps with the recurring task of constructing the "expected + vs. actual" error messages. It takes two arbitrary values and formats + corresponding strings with prettystr(). + + To keep the (possibly complex) output more readable in case the resulting + strings contain line breaks, they get automatically prefixed with additional + newlines. Both suffixes are optional (default to empty strings), and get + appended to the "value1" string. "suffix_a" is used if line breaks were + encountered, "suffix_b" otherwise. + + Returns the two formatted strings (including padding/newlines). + ]] + local str1, str2 = prettystr(value1), prettystr(value2) + if hasNewLine(str1) or hasNewLine(str2) then + -- line break(s) detected, add padding + return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 + end + return str1 .. (suffix_b or ""), str2 +end +M.private.prettystrPairs = prettystrPairs + +local UNKNOWN_REF = 'table 00-unknown ref' +local ref_generator = { value=1, [UNKNOWN_REF]=0 } + +local function table_ref( t ) + -- return the default tostring() for tables, with the table ID, even if the table has a metatable + -- with the __tostring converter + local ref = '' + local mt = getmetatable( t ) + if mt == nil then + ref = tostring(t) + else + local success, result + success, result = pcall(setmetatable, t, nil) + if not success then + -- protected table, if __tostring is defined, we can + -- not get the reference. And we can not know in advance. + ref = tostring(t) + if not ref:match( 'table: 0?x?[%x]+' ) then + return UNKNOWN_REF + end + else + ref = tostring(t) + setmetatable( t, mt ) + end + end + -- strip the "table: " part + ref = ref:sub(8) + if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then + -- Create a new reference number + ref_generator[ref] = ref_generator.value + ref_generator.value = ref_generator.value+1 + end + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + return string.format('table %02d-%s', ref_generator[ref], ref) + else + return string.format('table %02d', ref_generator[ref]) + end +end +M.private.table_ref = table_ref + +local TABLE_TOSTRING_SEP = ", " +local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) + +local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) + printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG + cycleDetectTable = cycleDetectTable or {} + cycleDetectTable[tbl] = true + + local result, dispOnMultLines = {}, false + + -- like prettystr but do not enclose with "" if the string is just alphanumerical + -- this is better for displaying table keys who are often simple strings + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) + end + + local mt = getmetatable( tbl ) + + if mt and mt.__tostring then + -- if table has a __tostring() function in its metatable, use it to display the table + -- else, compute a regular table + result = tostring(tbl) + if type(result) ~= 'string' then + return string.format( '', prettystr(result) ) + end + result = strsplit( '\n', result ) + return M.private._table_tostring_format_multiline_string( result, indentLevel ) + + else + -- no metatable, compute the table representation + + local entry, count, seq_index = nil, 0, 1 + for k, v in sortedPairs( tbl ) do + + -- key part + if k == seq_index then + -- for the sequential part of tables, we'll skip the "=" output + entry = '' + seq_index = seq_index + 1 + elseif cycleDetectTable[k] then + -- recursion in the key detected + cycleDetectTable.detected = true + entry = "<"..table_ref(k)..">=" + else + entry = keytostring(k) .. "=" + end + + -- value part + if cycleDetectTable[v] then + -- recursion in the value detected! + cycleDetectTable.detected = true + entry = entry .. "<"..table_ref(v)..">" + else + entry = entry .. + prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) + end + count = count + 1 + result[count] = entry + end + return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + end + +end +M.private._table_tostring = _table_tostring -- prettystr_sub() needs it + +local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) + local indentString = '\n'..string.rep(" ", indentLevel - 1) + return table.concat( tbl_str, indentString ) + +end +M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string + + +local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + -- final function called in _table_to_string() to format the resulting list of + -- string describing the table. + + local dispOnMultLines = false + + -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values + local totalLength = 0 + for k, v in ipairs( result ) do + totalLength = totalLength + string.len( v ) + if totalLength >= M.LINE_LENGTH then + dispOnMultLines = true + break + end + end + + -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded + -- with the values and the separators. + if not dispOnMultLines then + -- adjust with length of separator(s): + -- two items need 1 sep, three items two seps, ... plus len of '{}' + if #result > 0 then + totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) + end + dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) + end + + -- now reformat the result table (currently holding element strings) + if dispOnMultLines then + local indentString = string.rep(" ", indentLevel - 1) + result = { + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}" + } + else + result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} + end + if printTableRefs then + table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref + end + return table.concat(result) +end +M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it + +local function table_findkeyof(t, element) + -- Return the key k of the given element in table t, so that t[k] == element + -- (or `nil` if element is not present within t). Note that we use our + -- 'general' is_equal comparison for matching, so this function should + -- handle table-type elements gracefully and consistently. + if type(t) == "table" then + for k, v in pairs(t) do + if M.private.is_table_equals(v, element) then + return k + end + end + end + return nil +end + +local function _is_table_items_equals(actual, expected ) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false + + elseif (type_a == 'table') --[[and (type_e == 'table')]] then + for k, v in pairs(actual) do + if table_findkeyof(expected, v) == nil then + return false -- v not contained in expected + end + end + for k, v in pairs(expected) do + if table_findkeyof(actual, v) == nil then + return false -- v not contained in actual + end + end + return true + + elseif actual ~= expected then + return false + end + + return true +end + +--[[ +This is a specialized metatable to help with the bookkeeping of recursions +in _is_table_equals(). It provides an __index table that implements utility +functions for easier management of the table. The "cached" method queries +the state of a specific (actual,expected) pair; and the "store" method sets +this state to the given value. The state of pairs not "seen" / visited is +assumed to be `nil`. +]] +local _recursion_cache_MT = { + __index = { + -- Return the cached value for an (actual,expected) pair (or `nil`) + cached = function(t, actual, expected) + local subtable = t[actual] or {} + return subtable[expected] + end, + + -- Store cached value for a specific (actual,expected) pair. + -- Returns the value, so it's easy to use for a "tailcall" (return ...). + store = function(t, actual, expected, value, asymmetric) + local subtable = t[actual] + if not subtable then + subtable = {} + t[actual] = subtable + end + subtable[expected] = value + + -- Unless explicitly marked "asymmetric": Consider the recursion + -- on (expected,actual) to be equivalent to (actual,expected) by + -- default, and thus cache the value for both. + if not asymmetric then + t:store(expected, actual, value, true) + end + + return value + end + } +} + +local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual) + --[[Returns true if both table are equal. + + If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead + of strict equality. + + cycleDetectTable is an internal argument used during recursion on tables. + ]] + --print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected).. + -- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )') + + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false -- different types won't match + end + + if type_a == 'number' then + if marginForAlmostEqual ~= nil then + return M.almostEquals(actual, expected, marginForAlmostEqual) + else + return actual == expected + end + elseif type_a ~= 'table' then + -- other types compare directly + return actual == expected + end + + cycleDetectTable = cycleDetectTable or { actual={}, expected={} } + if cycleDetectTable.actual[ actual ] then + -- oh, we hit a cycle in actual + if cycleDetectTable.expected[ expected ] then + -- uh, we hit a cycle at the same time in expected + -- so the two tables have similar structure + return true + end + + -- cycle was hit only in actual, the structure differs from expected + return false + end + + if cycleDetectTable.expected[ expected ] then + -- no cycle in actual, but cycle in expected + -- the structure differ + return false + end + + -- at this point, no table cycle detected, we are + -- seeing this table for the first time + + -- mark the cycle detection + cycleDetectTable.actual[ actual ] = true + cycleDetectTable.expected[ expected ] = true + + + local actualKeysMatched = {} + for k, v in pairs(actual) do + actualKeysMatched[k] = true -- Keep track of matched keys + if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then + -- table differs on this key + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + end + + for k, v in pairs(expected) do + if not actualKeysMatched[k] then + -- Found a key that we did not see in "actual" -> mismatch + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + -- Otherwise actual[k] was already matched against v = expected[k]. + end + + -- all key match, we have a match ! + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return true +end +M.private._is_table_equals = _is_table_equals + +local function failure(main_msg, extra_msg_or_nil, level) + -- raise an error indicating a test failure + -- for error() compatibility we adjust "level" here (by +1), to report the + -- calling context + local msg + if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. '\n' .. main_msg + else + msg = main_msg + end + error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end + +local function is_table_equals(actual, expected, marginForAlmostEqual) + return _is_table_equals(actual, expected, nil, marginForAlmostEqual) +end +M.private.is_table_equals = is_table_equals + +local function fail_fmt(level, extra_msg_or_nil, ...) + -- failure with printf-style formatted message and given error level + failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) +end +M.private.fail_fmt = fail_fmt + +local function error_fmt(level, ...) + -- printf-style error() + error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end +M.private.error_fmt = error_fmt + +---------------------------------------------------------------- +-- +-- assertions +-- +---------------------------------------------------------------- + +local function errorMsgEquality(actual, expected, doDeepAnalysis, margin) + -- margin is supplied only for almost equal verification + + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + if type(expected) == 'string' or type(expected) == 'table' then + local strExpected, strActual = prettystrPairs(expected, actual) + local result = string.format("expected: %s\nactual: %s", strExpected, strActual) + if margin then + result = result .. '\nwere not equal by the margin of: '..prettystr(margin) + end + + -- extend with mismatch analysis if possible: + local success, mismatchResult + success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis, margin ) + if success then + result = table.concat( { result, mismatchResult }, '\n' ) + end + return result + end + return string.format("expected: %s, actual: %s", + prettystr(expected), prettystr(actual)) +end + +function M.assertError(f, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + if pcall( f, ... ) then + failure( "Expected an error when calling function but no error generated", nil, 2 ) + end +end + +function M.fail( msg ) + -- stops a test due to a failure + failure( msg, nil, 2 ) +end + +function M.failIf( cond, msg ) + -- Fails a test with "msg" if condition is true + if cond then + failure( msg, nil, 2 ) + end +end + +function M.skip(msg) + -- skip a running test + error_fmt(2, M.SKIP_PREFIX .. msg) +end + +function M.skipIf( cond, msg ) + -- skip a running test if condition is met + if cond then + error_fmt(2, M.SKIP_PREFIX .. msg) + end +end + +function M.runOnlyIf( cond, msg ) + -- continue a running test if condition is met, else skip it + if not cond then + error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) + end +end + +function M.success() + -- stops a test with a success + error_fmt(2, M.SUCCESS_PREFIX) +end + +function M.successIf( cond ) + -- stops a test with a success if condition is met + if cond then + error_fmt(2, M.SUCCESS_PREFIX) + end +end + + +------------------------------------------------------------------ +-- Equality assertions +------------------------------------------------------------------ + +function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) + end + elseif type(actual) ~= type(expected) then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + elseif actual ~= expected then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + end +end + +function M.almostEquals( actual, expected, margin ) + if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then + error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end + if margin < 0 then + error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin) + end + return math.abs(expected - actual) <= margin +end + +function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are close by margin + margin = margin or M.EPS + if type(margin) ~= 'number' then + error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin)) + end + + if type(actual) == 'table' and type(expected) == 'table' then + -- handle almost equals for table + if not is_table_equals(actual, expected, margin) then + failure( errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2 ) + end + elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then + if not M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. + 'Actual: %s, expected: %s, delta %s above margin of %s', + actual, expected, delta, margin) + end + else + error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end +end + +function M.assertNotEquals(actual, expected, extra_msg_or_nil) + if type(actual) ~= type(expected) then + return + end + + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + return + end + elseif actual ~= expected then + return + end + fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) +end + +function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are not close by margin + margin = margin or M.EPS + if M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. + ', delta %s below margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertItemsEquals(actual, expected, extra_msg_or_nil) + -- checks that the items of table expected + -- are contained in table actual. Warning, this function + -- is at least O(n^2) + if not _is_table_items_equals(actual, expected ) then + expected, actual = prettystrPairs(expected, actual) + fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', + expected, actual) + end +end + +------------------------------------------------------------------ +-- String assertion +------------------------------------------------------------------ + +function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) + -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) + if not string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if not string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', + sub, str) + end +end + +function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', + sub, str) + end +end + +function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) + -- Verify a full match for the string + if not strMatch( str, pattern, start, final ) then + pattern, str = prettystrPairs(pattern, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', + pattern, str) + end +end + +local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) + end + if type(expectedMsg) == "string" and type(error_msg) ~= "string" then + -- table are converted to string automatically + error_msg = tostring(error_msg) + end + local differ = false + if stripFileAndLine then + if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then + differ = true + end + else + if error_msg ~= expectedMsg then + local tr = type(error_msg) + local te = type(expectedMsg) + if te == 'table' then + if tr ~= 'table' then + differ = true + else + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end + end + else + differ = true + end + end + end + + if differ then + error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) + fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +function M.assertErrorMsgEquals( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + _assertErrorMsgEquals(false, expectedMsg, func, ...) +end + +function M.assertErrorMsgContentEquals(expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) +end + +function M.assertErrorMsgContains( partialMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not string.find( error_msg, partialMsg, nil, true ) then + error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) + fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', + partialMsg, error_msg) + end +end + +function M.assertErrorMsgMatches( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not strMatch( error_msg, expectedMsg ) then + expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) + fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +------------------------------------------------------------------ +-- Type assertions +------------------------------------------------------------------ + +function M.assertEvalToTrue(value, extra_msg_or_nil) + if not value then + failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertEvalToFalse(value, extra_msg_or_nil) + if value then + failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsTrue(value, extra_msg_or_nil) + if value ~= true then + failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsTrue(value, extra_msg_or_nil) + if value == true then + failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsFalse(value, extra_msg_or_nil) + if value ~= false then + failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsFalse(value, extra_msg_or_nil) + if value == false then + failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsNil(value, extra_msg_or_nil) + if value ~= nil then + failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNil(value, extra_msg_or_nil) + if value == nil then + failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) + end +end + +--[[ +Add type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type matches the +expected string (derived from the function name): + +M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" +]] +for _, funcName in ipairs( + {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', + 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} +) do + local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeExpected = typeExpected and typeExpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) ~= typeExpected then + if type(value) == 'nil' then + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', + typeExpected, type(value), prettystrPairs(value)) + else + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', + typeExpected, type(value), prettystrPairs(value)) + end + end + end +end + +--[[ +Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) +M.isXxx(value) -> returns true if type(value) conforms to "xxx" +]] +for _, typeExpected in ipairs( + {'Number', 'String', 'Table', 'Boolean', + 'Function', 'Userdata', 'Thread', 'Nil' } +) do + local typeExpectedLower = typeExpected:lower() + local isType = function(value) + return (type(value) == typeExpectedLower) + end + M['is'..typeExpected] = isType + M['is_'..typeExpectedLower] = isType +end + +--[[ +Add non-type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type differs from the +expected string (derived from the function name): + +M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" +]] +for _, funcName in ipairs( + {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', + 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} +) do + local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeUnexpected = typeUnexpected and typeUnexpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) == typeUnexpected then + fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', + typeUnexpected, prettystrPairs(value)) + end + end +end + +function M.assertIs(actual, expected, extra_msg_or_nil) + if actual ~= expected then + if not M.ORDER_ACTUAL_EXPECTED then + actual, expected = expected, actual + end + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + expected, actual = prettystrPairs(expected, actual, '\n', '') + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', + expected, actual) + end +end + +function M.assertNotIs(actual, expected, extra_msg_or_nil) + if actual == expected then + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + local s_expected + if not M.ORDER_ACTUAL_EXPECTED then + s_expected = prettystrPairs(actual) + else + s_expected = prettystrPairs(expected) + end + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) + end +end + + +------------------------------------------------------------------ +-- Scientific assertions +------------------------------------------------------------------ + + +function M.assertIsNaN(value, extra_msg_or_nil) + if type(value) ~= "number" or value == value then + failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNaN(value, extra_msg_or_nil) + if type(value) == "number" and value ~= value then + failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) + end +end + +function M.assertIsInf(value, extra_msg_or_nil) + if type(value) ~= "number" or math.abs(value) ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsMinusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= -math.huge then + failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsPlusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == math.huge then + failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == -math.huge then + failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsInf(value, extra_msg_or_nil) + if type(value) == "number" and math.abs(value) == math.huge then + failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == -math.huge) then + -- more precise error diagnosis + failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) + else if (1/value ~= math.huge) then + -- strange, case should have already been covered + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertIsMinusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == math.huge) then + -- more precise error diagnosis + failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) + else if (1/value ~= -math.huge) then + -- strange, case should have already been covered + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertNotIsPlusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == math.huge) then + failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == -math.huge) then + failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) + end +end + +function M.assertTableContains(t, expected, extra_msg_or_nil) + -- checks that table t contains the expected element + if table_findkeyof(t, expected) == nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s', + t, expected) + end +end + +function M.assertNotTableContains(t, expected, extra_msg_or_nil) + -- checks that table t doesn't contain the expected element + local k = table_findkeyof(t, expected) + if k ~= nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)', + t, expected, prettystr(k)) + end +end + +---------------------------------------------------------------- +-- Compatibility layer +---------------------------------------------------------------- + +-- for compatibility with LuaUnit v2.x +function M.wrapFunctions() + -- In LuaUnit version <= 2.1 , this function was necessary to include + -- a test function inside the global test suite. Nowadays, the functions + -- are simply run directly as part of the test discovery process. + -- so just do nothing ! + io.stderr:write[[Use of WrapFunctions() is no longer needed. +Just prefix your test function names with "test" or "Test" and they +will be picked up and run by LuaUnit. +]] +end + +local list_of_funcs = { + -- { official function name , alias } + + -- general assertions + { 'assertEquals' , 'assert_equals' }, + { 'assertItemsEquals' , 'assert_items_equals' }, + { 'assertNotEquals' , 'assert_not_equals' }, + { 'assertAlmostEquals' , 'assert_almost_equals' }, + { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, + { 'assertEvalToTrue' , 'assert_eval_to_true' }, + { 'assertEvalToFalse' , 'assert_eval_to_false' }, + { 'assertStrContains' , 'assert_str_contains' }, + { 'assertStrIContains' , 'assert_str_icontains' }, + { 'assertNotStrContains' , 'assert_not_str_contains' }, + { 'assertNotStrIContains' , 'assert_not_str_icontains' }, + { 'assertStrMatches' , 'assert_str_matches' }, + { 'assertError' , 'assert_error' }, + { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, + { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, + { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, + { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, + { 'assertIs' , 'assert_is' }, + { 'assertNotIs' , 'assert_not_is' }, + { 'assertTableContains' , 'assert_table_contains' }, + { 'assertNotTableContains' , 'assert_not_table_contains' }, + { 'wrapFunctions' , 'WrapFunctions' }, + { 'wrapFunctions' , 'wrap_functions' }, + + -- type assertions: assertIsXXX -> assert_is_xxx + { 'assertIsNumber' , 'assert_is_number' }, + { 'assertIsString' , 'assert_is_string' }, + { 'assertIsTable' , 'assert_is_table' }, + { 'assertIsBoolean' , 'assert_is_boolean' }, + { 'assertIsNil' , 'assert_is_nil' }, + { 'assertIsTrue' , 'assert_is_true' }, + { 'assertIsFalse' , 'assert_is_false' }, + { 'assertIsNaN' , 'assert_is_nan' }, + { 'assertIsInf' , 'assert_is_inf' }, + { 'assertIsPlusInf' , 'assert_is_plus_inf' }, + { 'assertIsMinusInf' , 'assert_is_minus_inf' }, + { 'assertIsPlusZero' , 'assert_is_plus_zero' }, + { 'assertIsMinusZero' , 'assert_is_minus_zero' }, + { 'assertIsFunction' , 'assert_is_function' }, + { 'assertIsThread' , 'assert_is_thread' }, + { 'assertIsUserdata' , 'assert_is_userdata' }, + + -- type assertions: assertIsXXX -> assertXxx + { 'assertIsNumber' , 'assertNumber' }, + { 'assertIsString' , 'assertString' }, + { 'assertIsTable' , 'assertTable' }, + { 'assertIsBoolean' , 'assertBoolean' }, + { 'assertIsNil' , 'assertNil' }, + { 'assertIsTrue' , 'assertTrue' }, + { 'assertIsFalse' , 'assertFalse' }, + { 'assertIsNaN' , 'assertNaN' }, + { 'assertIsInf' , 'assertInf' }, + { 'assertIsPlusInf' , 'assertPlusInf' }, + { 'assertIsMinusInf' , 'assertMinusInf' }, + { 'assertIsPlusZero' , 'assertPlusZero' }, + { 'assertIsMinusZero' , 'assertMinusZero'}, + { 'assertIsFunction' , 'assertFunction' }, + { 'assertIsThread' , 'assertThread' }, + { 'assertIsUserdata' , 'assertUserdata' }, + + -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) + { 'assertIsNumber' , 'assert_number' }, + { 'assertIsString' , 'assert_string' }, + { 'assertIsTable' , 'assert_table' }, + { 'assertIsBoolean' , 'assert_boolean' }, + { 'assertIsNil' , 'assert_nil' }, + { 'assertIsTrue' , 'assert_true' }, + { 'assertIsFalse' , 'assert_false' }, + { 'assertIsNaN' , 'assert_nan' }, + { 'assertIsInf' , 'assert_inf' }, + { 'assertIsPlusInf' , 'assert_plus_inf' }, + { 'assertIsMinusInf' , 'assert_minus_inf' }, + { 'assertIsPlusZero' , 'assert_plus_zero' }, + { 'assertIsMinusZero' , 'assert_minus_zero' }, + { 'assertIsFunction' , 'assert_function' }, + { 'assertIsThread' , 'assert_thread' }, + { 'assertIsUserdata' , 'assert_userdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_is_xxx + { 'assertNotIsNumber' , 'assert_not_is_number' }, + { 'assertNotIsString' , 'assert_not_is_string' }, + { 'assertNotIsTable' , 'assert_not_is_table' }, + { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, + { 'assertNotIsNil' , 'assert_not_is_nil' }, + { 'assertNotIsTrue' , 'assert_not_is_true' }, + { 'assertNotIsFalse' , 'assert_not_is_false' }, + { 'assertNotIsNaN' , 'assert_not_is_nan' }, + { 'assertNotIsInf' , 'assert_not_is_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_is_function' }, + { 'assertNotIsThread' , 'assert_not_is_thread' }, + { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, + + -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) + { 'assertNotIsNumber' , 'assertNotNumber' }, + { 'assertNotIsString' , 'assertNotString' }, + { 'assertNotIsTable' , 'assertNotTable' }, + { 'assertNotIsBoolean' , 'assertNotBoolean' }, + { 'assertNotIsNil' , 'assertNotNil' }, + { 'assertNotIsTrue' , 'assertNotTrue' }, + { 'assertNotIsFalse' , 'assertNotFalse' }, + { 'assertNotIsNaN' , 'assertNotNaN' }, + { 'assertNotIsInf' , 'assertNotInf' }, + { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, + { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, + { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, + { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, + { 'assertNotIsFunction' , 'assertNotFunction' }, + { 'assertNotIsThread' , 'assertNotThread' }, + { 'assertNotIsUserdata' , 'assertNotUserdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_xxx + { 'assertNotIsNumber' , 'assert_not_number' }, + { 'assertNotIsString' , 'assert_not_string' }, + { 'assertNotIsTable' , 'assert_not_table' }, + { 'assertNotIsBoolean' , 'assert_not_boolean' }, + { 'assertNotIsNil' , 'assert_not_nil' }, + { 'assertNotIsTrue' , 'assert_not_true' }, + { 'assertNotIsFalse' , 'assert_not_false' }, + { 'assertNotIsNaN' , 'assert_not_nan' }, + { 'assertNotIsInf' , 'assert_not_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_function' }, + { 'assertNotIsThread' , 'assert_not_thread' }, + { 'assertNotIsUserdata' , 'assert_not_userdata' }, + + -- all assertions with Coroutine duplicate Thread assertions + { 'assertIsThread' , 'assertIsCoroutine' }, + { 'assertIsThread' , 'assertCoroutine' }, + { 'assertIsThread' , 'assert_is_coroutine' }, + { 'assertIsThread' , 'assert_coroutine' }, + { 'assertNotIsThread' , 'assertNotIsCoroutine' }, + { 'assertNotIsThread' , 'assertNotCoroutine' }, + { 'assertNotIsThread' , 'assert_not_is_coroutine' }, + { 'assertNotIsThread' , 'assert_not_coroutine' }, +} + +-- Create all aliases in M +for _,v in ipairs( list_of_funcs ) do + local funcname, alias = v[1], v[2] + M[alias] = M[funcname] + + if EXPORT_ASSERT_TO_GLOBALS then + _G[funcname] = M[funcname] + _G[alias] = M[funcname] + end +end + +---------------------------------------------------------------- +-- +-- Outputters +-- +---------------------------------------------------------------- + +-- A common "base" class for outputters +-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html + +local genericOutput = { __class__ = 'genericOutput' } -- class +local genericOutput_MT = { __index = genericOutput } -- metatable +M.genericOutput = genericOutput -- publish, so that custom classes may derive from it + +function genericOutput.new(runner, default_verbosity) + -- runner is the "parent" object controlling the output, usually a LuaUnit instance + local t = { runner = runner } + if runner then + t.result = runner.result + t.verbosity = runner.verbosity or default_verbosity + t.fname = runner.fname + else + t.verbosity = default_verbosity + end + return setmetatable( t, genericOutput_MT) +end + +-- abstract ("empty") methods +function genericOutput:startSuite() + -- Called once, when the suite is started +end + +function genericOutput:startClass(className) + -- Called each time a new test class is started +end + +function genericOutput:startTest(testName) + -- called each time a new test is started, right before the setUp() + -- the current test status node is already created and available in: self.result.currentNode +end + +function genericOutput:updateStatus(node) + -- called with status failed or error as soon as the error/failure is encountered + -- this method is NOT called for a successful test because a test is marked as successful by default + -- and does not need to be updated +end + +function genericOutput:endTest(node) + -- called when the test is finished, after the tearDown() method +end + +function genericOutput:endClass() + -- called when executing the class is finished, before moving on to the next class of at the end of the test execution +end + +function genericOutput:endSuite() + -- called at the end of the test suite execution +end + + +---------------------------------------------------------------- +-- class TapOutput +---------------------------------------------------------------- + +local TapOutput = genericOutput.new() -- derived class +local TapOutput_MT = { __index = TapOutput } -- metatable +TapOutput.__class__ = 'TapOutput' + + -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html + + function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable( t, TapOutput_MT) + end + function TapOutput:startSuite() + print("1.."..self.result.selectedCount) + print('# Started on '..self.result.startDate) + end + function TapOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + + function TapOutput:updateStatus( node ) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" ) + return + end + + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print( prefixString( '# ', node.msg ) ) + end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print( prefixString( '# ', node.stackTrace ) ) + end + end + + function TapOutput:endTest( node ) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + end + end + + function TapOutput:endSuite() + print( '# '..M.LuaUnit.statusLine( self.result ) ) + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class JUnitOutput +---------------------------------------------------------------- + +-- See directory junitxml for more information about the junit format +local JUnitOutput = genericOutput.new() -- derived class +local JUnitOutput_MT = { __index = JUnitOutput } -- metatable +JUnitOutput.__class__ = 'JUnitOutput' + + function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable( t, JUnitOutput_MT ) + end + + function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error('With Junit, an output filename must be supplied with --name!') + end + if string.sub(self.fname,-4) ~= '.xml' then + self.fname = self.fname..'.xml' + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: "..self.fname) + end + + print('# XML output to '..self.fname) + print('# Started on '..self.result.startDate) + end + function JUnitOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + function JUnitOutput:startTest(testName) + print('# Starting test: '..testName) + end + + function JUnitOutput:updateStatus( node ) + if node:isFailure() then + print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + end + end + + function JUnitOutput:endSuite() + print( '# '..M.LuaUnit.statusLine(self.result)) + + -- XML file writing + self.fd:write('\n') + self.fd:write('\n') + self.fd:write(string.format( + ' \n', + self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION ) ) + self.fd:write(string.format(' \n', M.VERSION) ) + -- XXX please include system name and version if possible + self.fd:write(" \n") + + for i,node in ipairs(self.result.allTests) do + self.fd:write(string.format(' \n', + node.className, node.testName, node.duration ) ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) + end + self.fd:write(' \n') + end + + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(' \n') + self.fd:write(' \n') + + self.fd:write(' \n') + self.fd:write('\n') + self.fd:close() + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class TextOutput +---------------------------------------------------------------- + +--[[ Example of other unit-tests suite text output + +-- Python Non verbose: + +For each test: . or F or E + +If some failed tests: + ============== + ERROR / FAILURE: TestName (testfile.testclass) + --------- + Stack trace + + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Python Verbose: +testname (filename.classname) ... ok +testname (filename.classname) ... FAIL +testname (filename.classname) ... ERROR + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Ruby: +Started + . + Finished in 0.002695 seconds. + + 1 tests, 2 assertions, 0 failures, 0 errors + +-- Ruby: +>> ruby tc_simple_number2.rb +Loaded suite tc_simple_number2 +Started +F.. +Finished in 0.038617 seconds. + + 1) Failure: +test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: +Adding doesn't work. +<3> expected but was +<4>. + +3 tests, 4 assertions, 1 failures, 0 errors + +-- Java Junit +.......F. +Time: 0,003 +There was 1 failure: +1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError + at junit.samples.VectorTest.testCapacity(VectorTest.java:87) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + +FAILURES!!! +Tests run: 8, Failures: 1, Errors: 0 + + +-- Maven + +# mvn test +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running math.AdditionTest +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: +0.03 sec <<< FAILURE! + +Results : + +Failed tests: + testLireSymbole(math.AdditionTest) + +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 + + +-- LuaUnit +---- non verbose +* display . or F or E when running tests +---- verbose +* display test name + ok/fail +---- +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +* blank line +* number) ERROR or FAILURE: TestName + Stack trace + +then -------------- +then "Ran x tests in 0.000s (%d not selected, %d skipped)" +then OK or FAILED (failures=1, error=1) + + +]] + +local TextOutput = genericOutput.new() -- derived class +local TextOutput_MT = { __index = TextOutput } -- metatable +TextOutput.__class__ = 'TextOutput' + + function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable( t, TextOutput_MT ) + end + + function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print( 'Started on '.. self.result.startDate ) + end + end + + function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write( " ", self.result.currentNode.testName, " ... " ) + end + end + + function TextOutput:endTest( node ) + if node:isSuccess() then + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write("Ok\n") + else + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.status ) + print( node.msg ) + --[[ + -- find out when to do this: + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.stackTrace ) + end + ]] + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() + end + end + end + + function TextOutput:displayOneFailedTest( index, fail ) + print(index..") "..fail.testName ) + print( fail.msg ) + print( fail.stackTrace ) + print() + end + + function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else + print() + end + self:displayErroredTests() + self:displayFailedTests() + print( M.LuaUnit.statusLine( self.result ) ) + if self.result.notSuccessCount == 0 then + print('OK') + end + end + +-- class TextOutput end + + +---------------------------------------------------------------- +-- class NilOutput +---------------------------------------------------------------- + +local function nopCallable() + --print(42) + return nopCallable +end + +local NilOutput = { __class__ = 'NilOuptut' } -- class +local NilOutput_MT = { __index = nopCallable } -- metatable + +function NilOutput.new(runner) + return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) +end + +---------------------------------------------------------------- +-- +-- class LuaUnit +-- +---------------------------------------------------------------- + +M.LuaUnit = { + outputType = TextOutput, + verbosity = M.VERBOSITY_DEFAULT, + __class__ = 'LuaUnit', + instances = {} +} +local LuaUnit_MT = { __index = M.LuaUnit } + +if EXPORT_ASSERT_TO_GLOBALS then + LuaUnit = M.LuaUnit +end + + function M.LuaUnit.new() + local newInstance = setmetatable( {}, LuaUnit_MT ) + return newInstance + end + + -----------------[[ Utility methods ]]--------------------- + + function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if 'function' == type(aObject) then + return aObject + end + end + + function M.LuaUnit.splitClassMethod(someName) + --[[ + Return a pair of className, methodName strings for a name in the form + "class.method". If no class part (or separator) is found, will return + nil, someName instead (the latter being unchanged). + + This convention thus also replaces the older isClassMethod() test: + You just have to check for a non-nil className (return) value. + ]] + local separator = string.find(someName, '.', 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) + end + return nil, someName + end + + function M.LuaUnit.isMethodTestName( s ) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.isTestName( s ) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName( k ) then + table.insert( testNames , k ) + end + end + table.sort( testNames ) + return testNames + end + + function M.LuaUnit.parseCmdLine( cmdLine ) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns + + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + + if cmdLine == nil then + return result + end + + local function parseOption( option ) + if option == '--help' or option == '-h' then + result['help'] = true + return + elseif option == '--version' then + result['version'] = true + return + elseif option == '--verbose' or option == '-v' then + result['verbosity'] = M.VERBOSITY_VERBOSE + return + elseif option == '--quiet' or option == '-q' then + result['verbosity'] = M.VERBOSITY_QUIET + return + elseif option == '--error' or option == '-e' then + result['quitOnError'] = true + return + elseif option == '--failure' or option == '-f' then + result['quitOnFailure'] = true + return + elseif option == '--shuffle' or option == '-s' then + result['shuffle'] = true + return + elseif option == '--output' or option == '-o' then + state = SET_OUTPUT + return state + elseif option == '--name' or option == '-n' then + state = SET_FNAME + return state + elseif option == '--repeat' or option == '-r' then + state = SET_REPEAT + return state + elseif option == '--pattern' or option == '-p' then + state = SET_PATTERN + return state + elseif option == '--exclude' or option == '-x' then + state = SET_EXCLUDE + return state + end + error('Unknown option: '..option,3) + end + + local function setArg( cmdArg, state ) + if state == SET_OUTPUT then + result['output'] = cmdArg + return + elseif state == SET_FNAME then + result['fname'] = cmdArg + return + elseif state == SET_REPEAT then + result['exeRepeat'] = tonumber(cmdArg) + or error('Malformed -r argument: '..cmdArg) + return + elseif state == SET_PATTERN then + if result['pattern'] then + table.insert( result['pattern'], cmdArg ) + else + result['pattern'] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = '!'..cmdArg + if result['pattern'] then + table.insert( result['pattern'], notArg ) + else + result['pattern'] = { notArg } + end + return + end + error('Unknown parse state: '.. state) + end + + + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg( cmdArg, state, result ) + state = nil + else + if cmdArg:sub(1,1) == '-' then + state = parseOption( cmdArg ) + else + if result['testNames'] then + table.insert( result['testNames'], cmdArg ) + else + result['testNames'] = { cmdArg } + end + end + end + end + + if result['help'] then + M.LuaUnit.help() + end + + if result['version'] then + M.LuaUnit.version() + end + + if state ~= nil then + error('Missing argument after '..cmdLine[ #cmdLine ],2 ) + end + + return result + end + + function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) + end + + function M.LuaUnit.version() + print('LuaUnit v'..M.VERSION..' by Philippe Fremy ') + os.exit(0) + end + +---------------------------------------------------------------- +-- class NodeStatus +---------------------------------------------------------------- + + local NodeStatus = { __class__ = 'NodeStatus' } -- class + local NodeStatus_MT = { __index = NodeStatus } -- metatable + M.NodeStatus = NodeStatus + + -- values of status + NodeStatus.SUCCESS = 'SUCCESS' + NodeStatus.SKIP = 'SKIP' + NodeStatus.FAIL = 'FAIL' + NodeStatus.ERROR = 'ERROR' + + function NodeStatus.new( number, testName, className ) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable( t, NodeStatus_MT ) + t:success() + return t + end + + function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil + end + + function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil + end + + function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS + end + + function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) + end + + function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP + end + + function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL + end + + function NodeStatus:isError() + return self.status == NodeStatus.ERROR + end + + function NodeStatus:statusXML() + if self:isError() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isFailure() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isSkipped() then + return table.concat({' ', xmlEscape(self.msg),'\n' } ) + end + return ' \n' -- (not XSD-compliant! normally shouldn't get here) + end + + --------------[[ Output methods ]]------------------------- + + local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = '' + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == 'ss') and 'es' or 's' + end + return string.format('%d %s%s', number, singular, suffix) + end + + function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format('Ran %d tests in %0.3f seconds', + result.runCount, result.duration), + conditional_plural(result.successCount, 'success'), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, 'failure')) + end + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, 'error')) + end + else + table.insert(s, '0 failures') + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ', ') + end + + function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv('LUAUNIT_DATEFMT')), + startIsodate = os.date('%Y-%m-%dT%H:%M:%S'), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() + end + + function M.LuaUnit:startClass( className, classInstance ) + self.result.currentClassName = className + self.output:startClass( className ) + self:setupClass( className, classInstance ) + end + + function M.LuaUnit:startTest( testName ) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new( + self.result.currentTestNumber, + testName, + self.result.currentClassName + ) + self.result.currentNode.startTime = os.clock() + table.insert( self.result.allTests, self.result.currentNode ) + self.output:startTest( testName ) + end + + function M.LuaUnit:updateStatus( err ) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return + end + + local node = self.result.currentNode + + --[[ As a first approach, we will report only one error or one failure for one test. + + However, we can have the case where the test is in failure, and the teardown is in error. + In such case, it's a good idea to report both a failure and an error in the test suite. This is + what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for + example, there could be more (failures + errors) count that tests. What happens to the current node ? + + We will do this more intelligent version later. + ]] + + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return + end + + if err.status == NodeStatus.FAIL then + node:fail( err.msg, err.trace ) + table.insert( self.result.failedTests, node ) + elseif err.status == NodeStatus.ERROR then + node:error( err.msg, err.trace ) + table.insert( self.result.errorTests, node ) + elseif err.status == NodeStatus.SKIP then + node:skip( err.msg ) + table.insert( self.result.skippedTests, node ) + else + error('No such status: ' .. prettystr(err.status)) + end + + self.output:updateStatus( node ) + end + + function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest( node ) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error('No such node status: ' .. prettystr(node.status)) + end + self.result.currentNode = nil + end + + function M.LuaUnit:endClass() + self:teardownClass( self.lastClassName, self.lastClassInstance ) + self.output:endClass() + end + + function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error('LuaUnit:endSuite() -- suite was already ended' ) + end + self.result.duration = os.clock()-self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() + end + + function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error( 'No such format: '..outputType,2) + end + + --------------[[ Runner ]]----------------- + + function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2) + } + end + + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) + else + ok, err = xpcall( function () methodInstance() end, err_handler ) + end + if ok then + return {status = NodeStatus.SUCCESS} + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') + + local iter_msg + iter_msg = self.exeRepeat and 'iteration '..self.currentCount + + err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err + end + + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) + end + + return err -- return the error "object" (table) + end + + + function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set + + if type(methodInstance) ~= 'function' then + self:unregisterSuite() + error( tostring(methodName)..' must be a function, not '..type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = '[TestFunctions]' + prettyFuncName = methodName + else + prettyFuncName = className..'.'..methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() + end + self:startClass( className, classInstance ) + self.lastClassName = className + self.lastClassInstance = classInstance + end + + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) + if classInstance then + local func = self.asFunction( classInstance.setUp ) or + self.asFunction( classInstance.Setup ) or + self.asFunction( classInstance.setup ) or + self.asFunction( classInstance.SetUp ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) + end + end + + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) + end + + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction( classInstance.tearDown ) or + self.asFunction( classInstance.TearDown ) or + self.asFunction( classInstance.teardown ) or + self.asFunction( classInstance.Teardown ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) + end + end + end + + self:endTest() + end + + function M.LuaUnit.expandOneClass( result, className, classInstance ) + --[[ + Input: a list of { name, instance }, a class name, a class instance + Ouptut: modify result to add all test method instance in the form: + { className.methodName, classInstance } + ]] + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then + table.insert( result, { className..'.'..methodName, classInstance } ) + end + end + end + + function M.LuaUnit.expandClasses( listOfNameAndInst ) + --[[ + -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} + -- functions and methods remain untouched + + Input: a list of { name, instance } + + Output: + * { function name, function instance } : do nothing + * { class.method name, class instance }: do nothing + * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) + ]] + local result = {} + + for i,v in ipairs( listOfNameAndInst ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert( result, { name, instance } ) + else + if type(instance) ~= 'table' then + error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) + end + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + table.insert( result, { name, instance } ) + else + M.LuaUnit.expandOneClass( result, name, instance ) + end + end + end + + return result + end + + function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) + local included, excluded = {}, {} + for i, v in ipairs( listOfNameAndInst ) do + -- local name, instance = v[1], v[2] + if patternFilter( patternIncFilter, v[1] ) then + table.insert( included, v ) + else + table.insert( excluded, v ) + end + end + return included, excluded + end + + local function getKeyInListWithGlobalFallback( key, listOfNameAndInst ) + local result = nil + for i,v in ipairs( listOfNameAndInst ) do + if(listOfNameAndInst[i][1] == key) then + result = listOfNameAndInst[i][2] + break + end + end + if(not M.LuaUnit.asFunction( result ) ) then + result = _G[key] + end + return result + end + + function M.LuaUnit:setupSuite( listOfNameAndInst ) + local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst) + if self.asFunction( setupSuite ) then + self:updateStatus( self:protectedCall( nil, setupSuite, 'setupSuite' ) ) + end + end + + function M.LuaUnit:teardownSuite(listOfNameAndInst) + local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst) + if self.asFunction( teardownSuite ) then + self:updateStatus( self:protectedCall( nil, teardownSuite, 'teardownSuite') ) + end + end + + function M.LuaUnit:setupClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.setupClass ) then + self:updateStatus( self:protectedCall( instance, instance.setupClass, className..'.setupClass' ) ) + end + end + + function M.LuaUnit:teardownClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.teardownClass ) then + self:updateStatus( self:protectedCall( instance, instance.teardownClass, className..'.teardownClass' ) ) + end + end + + function M.LuaUnit:internalRunSuiteByInstances( listOfNameAndInst ) + --[[ Run an explicit list of tests. Each item of the list must be one of: + * { function name, function instance } + * { class name, class instance } + * { class.method name, class instance } + + This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances() + ]] + + local expandedList = self.expandClasses( listOfNameAndInst ) + if self.shuffle then + randomizeTable( expandedList ) + end + local filteredList, filteredOutList = self.applyPatternFilter( + self.patternIncludeFilter, expandedList ) + + self:startSuite( #filteredList, #filteredOutList ) + self:setupSuite( listOfNameAndInst ) + + for i,v in ipairs( filteredList ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction( nil, name, nil, instance ) + else + -- expandClasses() should have already taken care of sanitizing the input + assert( type(instance) == 'table' ) + local className, methodName = M.LuaUnit.splitClassMethod( name ) + assert( className ~= nil ) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction( className, methodName, instance, methodInstance ) + end + if self.result.aborted then + break -- "--error" or "--failure" option triggered + end + end + + if self.lastClassName ~= nil then + self:endClass() + end + + self:teardownSuite( listOfNameAndInst ) + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + self:unregisterSuite() + os.exit(-2) + end + end + + function M.LuaUnit:internalRunSuiteByNames( listOfName ) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + namespace analysis. Convert the list into a list of (name, valid instances (table or function)) + and calls internalRunSuiteByInstances. + ]] + + local instanceName, instance + local listOfNameAndInst = {} + + for i,name in ipairs( listOfName ) do + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + instanceName = className + instance = _G[instanceName] + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if type(instance) ~= 'table' then + self:unregisterSuite() + error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) + end + + local methodInstance = instance[methodName] + if methodInstance == nil then + self:unregisterSuite() + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] + end + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if (type(instance) ~= 'table' and type(instance) ~= 'function') then + self:unregisterSuite() + error( 'Name must match a function or a table: '..instanceName ) + end + + table.insert( listOfNameAndInst, { name, instance } ) + end + + self:internalRunSuiteByInstances( listOfNameAndInst ) + end + + function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + local runner = M.LuaUnit.new() + return runner:runSuite(...) + end + + function M.LuaUnit:registerSuite() + -- register the current instance into our global array of instances + -- print('-> Register suite') + M.LuaUnit.instances[ #M.LuaUnit.instances+1 ] = self + end + + function M.unregisterCurrentSuite() + -- force unregister the last registered suite + table.remove(M.LuaUnit.instances, #M.LuaUnit.instances) + end + + function M.LuaUnit:unregisterSuite() + -- print('<- Unregister suite') + -- remove our current instqances from the global array of instances + local instanceIdx = nil + for i, instance in ipairs(M.LuaUnit.instances) do + if instance == self then + instanceIdx = i + break + end + end + + if instanceIdx ~= nil then + table.remove(M.LuaUnit.instances, instanceIdx) + -- print('Unregister done') + end + + end + + function M.LuaUnit:initFromArguments( ... ) + --[[Parses all arguments from either command-line or direct call and set internal + flags of LuaUnit runner according to it. + + Return the list of names which were possibly passed on the command-line or as arguments + ]] + local args = {...} + if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument self to make it a command-line argument list + table.remove(args,1) + end + + if #args == 0 then + args = cmdline_argv + end + + local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + options.output = options.output or os.getenv('LUAUNIT_OUTPUT') + options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME') + + if options.output then + if options.output:lower() == 'junit' and options.fname == nil then + print('With junit output, a filename must be supplied with -n or --name') + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + return options.testNames + end + + function M.LuaUnit:runSuite( ... ) + testNames = self:initFromArguments(...) + self:registerSuite() + self:internalRunSuiteByNames( testNames or M.LuaUnit.collectTests() ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + function M.LuaUnit:runSuiteByInstances( listOfNameAndInst, commandLineArguments ) + --[[ + Run all test functions or tables provided as input. + + Input: a list of { name, instance } + instance can either be a function or a table containing test functions starting with the prefix "test" + + return the number of failures and errors, 0 meaning success + ]] + -- parse the command-line arguments + testNames = self:initFromArguments( commandLineArguments ) + self:registerSuite() + self:internalRunSuiteByInstances( listOfNameAndInst ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + + +-- class LuaUnit + +-- For compatbility with LuaUnit v2 +M.run = M.LuaUnit.run +M.Run = M.LuaUnit.run + +function M:setVerbosity( verbosity ) + -- set the verbosity value (as integer) + M.LuaUnit.verbosity = verbosity +end +M.set_verbosity = M.setVerbosity +M.SetVerbosity = M.setVerbosity + + +return M \ No newline at end of file diff --git a/luci-app-gpoint-main/test/matrix_test.lua b/luci-app-gpoint-main/test/matrix_test.lua new file mode 100644 index 000000000..a4b615a0c --- /dev/null +++ b/luci-app-gpoint-main/test/matrix_test.lua @@ -0,0 +1,46 @@ + +local matrix = require("matrix_lib") + + +function test_copy() + local foo = matrix.create(3, 3) + local bar = matrix.create(3, 3) + foo[1][1] = 1337.0 + bar = matrix.copy(foo, bar) + + assert(bar[1][1] == 1337.0) +end + +function test_inverse() + local foo = matrix.create(4, 4) + foo = matrix.set(foo, 1.0, 2.0, 3.0, 4.0, + 4.0, 1.0, 7.0, 9.0, + 0.0, 0.0, -4.0, -4.0, + 2.3, 3.4, 3.1, 0.0) + + local foo_copy = matrix.copy(foo) + local bar = matrix.create(4, 4) + local identity = matrix.create(4, 4) + + identity = matrix.set_identity(identity) + + matrix.print(foo) + print("--------------") + matrix.print(bar) + print("--------------") + assert(matrix.destructive_invert(foo, bar)) + matrix.print(foo) + print("--------------") + matrix.print(bar) + print("--------------") + + assert(matrix.equal(foo, identity, 0.0001)) + foo = matrix.multiply(foo_copy, bar, foo) + assert(matrix.equal(foo, identity, 0.0001)) + foo = matrix.multiply(bar, foo_copy, foo) + assert(matrix.equal(foo, identity, 0.0001)) +end + +test_copy() +test_inverse() +print("OK") \ No newline at end of file diff --git a/luci-app-gpoint-main/test/nmea_test.lua b/luci-app-gpoint-main/test/nmea_test.lua new file mode 100644 index 000000000..b6e4721ee --- /dev/null +++ b/luci-app-gpoint-main/test/nmea_test.lua @@ -0,0 +1,547 @@ +common_path = '/usr/share/gpoint/tests/luaunitlib/?.lua;' +package.path = common_path .. package.path + +lu = require('luaunit') +---------------------------------------------------------------------------- +-- nmea test + +-- Bitxor +-- checkcrc +-- getcropdata +---------------------------------------------------------------------------- + +function createGnssForm() + local GnssData = { + warning = {app, gga, rmc, vtg, gsa, gp} + } + return GnssData +end +-- To calculate the checksum +-- Bitwise xor +local function BitXOR(a, b) + local p, c = 1, 0 + while a > 0 and b > 0 do + local ra, rb = a % 2, b % 2 + if ra ~= rb then c = c + p end + a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 + end + + if a < b then a = b end + while a > 0 do + local ra = a % 2 + if ra > 0 then c = c + p end + a, p = (a - ra) / 2, p * 2 + end + return c +end + +-- To calculate the checksum +function decimalToHex(num) + if num == 0 then + return '0' + end + + local neg = false + if num < 0 then + neg = true + num = num * -1 + end + + local hexstr = "0123456789ABCDEF" + local result = "" + while num > 0 do + local n = math.mod(num, 16) + result = string.sub(hexstr, n + 1, n + 1) .. result + num = math.floor(num / 16) + end + + if neg then + result = '-' .. result + end + return result +end + +-- Сalculate the checksum (CRC-8) +function checkCRC(data) + local crc8 = string.sub(data, #data - 1) + data = string.sub(data, 2, #data - 3) + + local b_sum = string.byte(data, 1) + for i = 2, #data do + b_sum = BitXOR(b_sum, string.byte(data, i)) + end + + return decimalToHex(b_sum) == crc8 and true or false +end + +--Converting coordinates from the NMEA protocol to degrees +function nmeaCoordinatesToDouble(coord) + local deg = math.floor(coord / 100) + return deg + (coord - 100 * deg) / 60 +end + +--We are looking for the desired data line in the line received from the device +function findInResp(data, begin) + local err = true + local b = string.find(data, begin) + local e = string.find(data, "\r\n", b) + + if b and e then + err = false + else + b = nil + e = nil + end + return err, b, e +end + +function getCropData(data, msg) + local err, b, e = findInResp(data, msg) + if not err then + data = string.gsub(string.sub(data, b, e), '%c', "") + if checkCRC(data) then + data = string.gsub(data, msg, '', 1) + data = string.gsub(data, "*%d+%w+", '', 1) + err = {false, "OK"} + else + err = {true, "Checksum error"} + data = nil + end + else + err = {true, "No data found"} + data = nil + end + return err, data +end + +-- Creating a table with data before adding data to a single space +function doTable(data, keys) + local tabl = {} + + while string.find(data, ',,') do + data = string.gsub(data, ',,', ",-,") + end + + local i = 1 + for val in string.gmatch(data, "[^,]+") do + tabl[keys[i]] = val + i = i + 1 + end + return tabl +end + +-- The function of searching the time zone by the received coordinates +function findTimeZone(time, date, lon) + local datetime = { year,month,day,hour,min,sec } + -- calculate the time zone by coordinates + local timeZone = math.floor((tonumber(lon) + (7.5 * (tonumber(lon) > 0 and 1.0 or -1.0))) / 15.0) + + datetime.hour, datetime.min, datetime.sec = string.match(time, "(%d%d)(%d%d)(%d%d)") + datetime.day, datetime.month, datetime.year = string.match(date,"(%d%d)(%d%d)(%d%d)") + datetime.year = "20" .. datetime.year -- Someone change this to 21 in the 2100 year + + --we request the unix time and then add the time zone + local unix = os.time(datetime) + unix = unix + ((math.floor(timeZone* 100)) % 100) * 36 + unix = unix + math.floor(timeZone) * 3600 + + return os.date("*t", unix) +end +-- Add 0 for the time and date values if < 10 +function addZero(val) + return tonumber(val) > 9 and tostring(val) or '0' .. tostring(val) +end + +--------------------------------------------------------------------------------------------------------------- +-- GGA - Global Positioning System Fix Data +function getGGA(resp) + local err, gga = getCropData(resp, "$GPGGA,") + if not err[1] then + local ggakeys = { + "utc", -- UTC of this position report, hh is hours, mm is minutes, ss.ss is seconds. + "latitude", -- Latitude, dd is degrees, mm.mm is minutes + "ne", -- N or S (North or South) + "longitude", -- Longitude, dd is degrees, mm.mm is minutes + "ew", -- E or W (East or West) + "qual", -- GPS Quality Indicator (non null) + "sat", -- Number of satellites in use, 00 - 12 + "hdp", -- Horizontal Dilution of precision (meters) + "alt", -- Antenna Altitude above/below mean-sea-level (geoid) (in meters) + "ualt", -- Units of antenna altitude, meters + "gsep", -- Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level + "ugsep", -- Units of geoidal separation, meters + "age", -- Age of differential GPS data, time in seconds since last SC104 type 1 or 9 update, null field when DGPS is not used + "drs" -- Differential reference station ID, 0000-1023 + } + + if string.gsub(gga, ',', '') ~= '0' then + gga = doTable(gga, ggakeys) + else + err = {true, "Bad GGA data"} + gga = nil + end + end + return err, gga +end + +-- RMC - Recommended Minimum Navigation Information +function getRMC(resp) + local err, rmc = getCropData(resp, "$GPRMC,") + if not err[1] then + local rmckeys = { + "utc", -- UTC of position fix, hh is hours, mm is minutes, ss.ss is seconds. + "valid", -- Status, A = Valid, V = Warning + "latitude", -- Latitude, dd is degrees. mm.mm is minutes. + "ns", -- N or S + "longitude", -- Longitude, ddd is degrees. mm.mm is minutes. + "ew", -- E or W + "knots", -- Speed over ground, knots + "tmgdt", -- Track made good, degrees true + "date", -- Date, ddmmyy + "mv", -- Magnetic Variation, degrees + "ewm", -- E or W + "nstat" -- Nav Status (NMEA 4.1 and later) A=autonomous, D=differential, E=Estimated, -> + -- -> M=Manual input mode N=not valid, S=Simulator, V = Valid + } + + if not string.find(rmc, 'V') then + rmc = doTable(rmc, rmckeys) + else + err = {true, "Bad RMC data"} + rmc = nil + end + end + return err, rmc +end + +-- VTG - Track made good and Ground speed +function getVTG(resp) + local err, vtg = getCropData(resp, "$GPVTG,") + if not err[1] then + local vtgkeys = { + "course_t", -- Course over ground, degrees True + 't', -- T = True + "course_m", -- Course over ground, degrees Magnetic + 'm', -- M = Magnetic + "knots", -- Speed over ground, knots + 'n', -- N = Knots + "speed", -- Speed over ground, km/hr + 'k', -- K = Kilometers Per Hour + "faa" -- FAA mode indicator (NMEA 2.3 and later) + } + + if string.find(vtg, 'A') or string.find(vtg, 'D') then + vtg = doTable(vtg, vtgkeys) + else + err = {true, "Bad VTG data"} + vtg = nil + end + end + return err, vtg +end + +--GSA - GPS DOP and active satellites +function getGSA(resp) + local err, gsa = getCropData(resp, "$GPGSA,") + if not err[1] then + local gsakeys = { + "smode", -- Selection mode: M=Manual, forced to operate in 2D or 3D, A=Automatic, 2D/3D + "mode", -- Mode (1 = no fix, 2 = 2D fix, 3 = 3D fix) + "id1", -- ID of 1st satellite used for fix + "id2", -- ID of 2nd satellite used for fix + "id3", -- ID of 3rd satellite used for fix + "id4", -- ID of 4th satellite used for fix + "id5", -- ID of 5th satellite used for fix + "id6", -- ID of 6th satellite used for fix + "id7", -- ID of 7th satellite used for fix + "id8", -- ID of 8th satellite used for fix + "id9", -- ID of 9th satellite used for fix + "id10", -- ID of 10th satellite used for fix + "id11", -- ID of 11th satellite used for fix + "id12", -- ID of 12th satellite used for fix + "pdop", -- PDOP + "hdop", -- HDOP + "vdop" -- VDOP + } + + if string.find(gsa, '2') then + gsa = doTable(gsa, gsakeys) + else + err = {true, "Bad GSA data"} + gsa = nil + end + end + return err, gsa +end + +function parseAllData(resp) + local GnssData = createGnssForm() + GnssData.warning.gga, GnssData["GGA"] = getGGA(resp) + GnssData.warning.rmc, GnssData["RMC"] = getRMC(resp) + GnssData.warning.vtg, GnssData["VTG"] = getVTG(resp) + GnssData.warning.gsa, GnssData["GSA"] = getGSA(resp) + return GnssData +end + +-- This function prepares data for the web application (Some custom data) +function getGPoint(resp) + + local web = { + longitude = '-', + latitude = '-', + altitude = '-', + utc = '-', + date = '-', + nsat = '-', + hdop = '-', + cog = '-', + spkm = '-' + } + + local GnssData = parseAllData(resp) + local err = {true, ""} + + if not GnssData.warning.gga[1] then + web.latitude = string.format("%0.6f", nmeaCoordinatesToDouble(GnssData.GGA.latitude)) + web.longitude = string.format("%0.6f",nmeaCoordinatesToDouble(GnssData.GGA.longitude)) + web.altitude = GnssData.GGA.alt + web.nsat = GnssData.GGA.sat + web.hdop = GnssData.GGA.hdp + else + err[2] = "GGA: " .. GnssData.warning.gga[2] .. ' ' + end + + if not GnssData.warning.vtg[1] then + web.cog = GnssData.VTG.course_t + web.spkm = GnssData.VTG.speed + else + err[2] = err[2] .. "VTG: " .. GnssData.warning.vtg[2] .. ' ' + end + + if not GnssData.warning.rmc[1] then + local dateTime = findTimeZone(GnssData.RMC.utc, GnssData.RMC.date, nmeaCoordinatesToDouble(GnssData.RMC.longitude)) + + web.utc = string.format("%s:%s", addZero(dateTime.hour), addZero(dateTime.min)) + web.date = string.format("%s.%s.%d", addZero(dateTime.day), addZero(dateTime.month), dateTime.year) + else + err[2] = err[2] .. "RMC: " .. GnssData.warning.vtg[2] + end + + if GnssData.warning.gga[1] and GnssData.warning.vtg[1] and GnssData.warning.rmc[1] then + err[2] = "Updating data..." + else + err = {false, "OK"} + end + + return err, web +end + +----------------------------------------------------------------------------------- +-- public +----------------------------------------------------------------------------------- + +function getData(line, port) + local err, resp = serial.read(port) + local GnssData = createGnssForm() + if err[1] then + GnssData.warning.app = {true, err[2]} + return GnssData + end + + GnssData.warning.app = {false, "OK"} + + if line == "GP" then + GnssData.warning.gp, GnssData["gp"] = getGPoint(resp) + elseif line == "GGA" then + GnssData.warning.gga, GnssData["gga"] = getGGA(resp) + elseif line == "RMC" then + GnssData.warning.rmc, GnssData["rmc"] = getRMC(resp) + elseif line == "VTG" then + GnssData.warning.vtg, GnssData["vtg"] = getVTG(resp) + elseif line == "GSA" then + GnssData.warning.gsa, GnssData["gsa"] = getGSA(resp) + else + GnssData.warning.app = {true, "Bad argument..."} + end + return GnssData +end + +function getAllData(port) + local err, resp = serial.read(port) + local GnssData = createGnssForm() + if err[1] then + GnssData.warning.app = {true, err[2]} + return GnssData + end + + GnssData = parseAllData(resp) + GnssData.warning.gp, GnssData["gp"] = getGPoint(resp) + GnssData.warning.app = {false, "OK"} + return GnssData +end + +------------------------------------------------------------------------------ + +local goodGNSSSdata = { + "$GPRMC,113702.568,V,4154.931,N,08002.497,W,95.5,0.02,220721,,E*4E", + "$GPGGA,113703.568,4154.931,N,08002.497,W,0,00,,,M,,M,,*52", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.1,0.8,0.6*3F", + "$GPRMC,113705.568,V,4154.933,N,08002.497,W,86.0,-0.05,220721,,E*66", + "$GPGGA,113706.568,4154.933,N,08002.497,W,0,00,0.8,,M,,M,,*73", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.0,0.4*36", + "$GPRMC,113708.568,V,4154.935,N,08002.498,W,55.1,-0.10,220721,,E*69", + "$GPGGA,113709.568,4154.935,N,08002.498,W,0,00,0.0,,M,,M,,*7D", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.2,0.8*3C", + "$GPRMC,113711.568,V,4154.937,N,08002.498,W,95.0,-0.10,220721,,E*6E", + "$GPGGA,113712.568,4154.937,N,08002.498,W,0,00,0.2,,M,,M,,*77", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.7,0.6,0.3*32", + "$GPRMC,113714.568,V,4154.939,N,08002.498,W,28.0,-0.07,220721,,E*65", + "$GPGGA,113715.568,4154.939,N,08002.498,W,0,00,0.6,,M,,M,,*7A", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,1.0,0.5*34", + "$GPRMC,113717.568,V,4154.940,N,08002.498,W,30.1,0.03,220721,,E*49", + "$GPGGA,113718.568,4154.940,N,08002.498,W,0,00,1.0,,M,,M,,*7E", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.5,0.1,0.2*36", + "$GPRMC,113720.568,V,4154.942,N,08002.498,W,0.2,-0.02,220721,,E*53", + "$GPGGA,113721.568,4154.942,N,08002.498,W,0,00,0.1,,M,,M,,*76", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.5,0.7*30", + "$GPRMC,113723.568,V,4154.944,N,08002.498,W,53.6,0.05,220721,,E*4E", + "$GPGGA,113724.568,4154.944,N,08002.498,W,0,00,0.5,,M,,M,,*71", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,0.6,0.1*37", + "$GPRMC,113726.568,V,4154.946,N,08002.498,W,76.6,0.04,220721,,E*4F", + "$GPGGA,113727.568,4154.946,N,08002.498,W,0,00,0.6,,M,,M,,*73", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.2,0.3,0.7*37", + "$GPRMC,113729.568,V,4154.948,N,08002.497,W,30.9,0.12,220721,,E*4B", + "$GPGGA,113730.568,4154.948,N,08002.497,W,0,00,0.3,,M,,M,,*71", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.2,0.2*34", + "$GPRMC,113732.568,V,4154.949,N,08002.497,W,47.2,0.20,220721,,E*4A", + "$GPGGA,113733.568,4154.949,N,08002.497,W,0,00,0.2,,M,,M,,*72", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.3,0.8,0.2*39", + "$GPRMC,113735.568,V,4154.951,N,08002.496,W,31.1,0.13,220721,,E*47", + "$GPGGA,113736.568,4154.951,N,08002.496,W,0,00,0.8,,M,,M,,*75", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.6,0.1*35", + "$GPRMC,113738.568,V,4154.953,N,08002.496,W,58.2,0.10,220721,,E*47", + "$GPGGA,113739.568,4154.953,N,08002.496,W,0,00,0.6,,M,,M,,*76", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.6,0.9,0.7*39", + "$GPRMC,113741.568,V,4154.955,N,08002.496,W,88.3,0.03,220721,,E*41", + "$GPGGA,113742.568,4154.955,N,08002.496,W,0,00,0.9,,M,,M,,*73", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.8,0.8,0.1*30", + "$GPRMC,113744.568,V,4154.956,N,08002.496,W,89.3,0.10,220721,,E*44", + "$GPGGA,113745.568,4154.956,N,08002.496,W,0,00,0.8,,M,,M,,*76", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.9,0.2,1.0*3B", + "$GPRMC,113747.568,V,4154.958,N,08002.495,W,99.1,0.14,220721,,E*4D", + "$GPGGA,113748.568,4154.958,N,08002.495,W,0,00,0.2,,M,,M,,*7C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.1,0.2*35", + "$GPRMC,113750.568,V,4154.960,N,08002.495,W,84.0,0.19,220721,,E*40", + "$GPGGA,113751.568,4154.960,N,08002.495,W,0,00,0.1,,M,,M,,*7C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.3,1.0,0.5*36", + "$GPRMC,113753.568,V,4154.962,N,08002.495,W,24.0,0.13,220721,,E*41", + "$GPGGA,113754.568,4154.962,N,08002.495,W,0,00,1.0,,M,,M,,*7B", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.8,0.9*37", + "$GPRMC,113756.568,V,4154.963,N,08002.494,W,27.8,0.03,220721,,E*4E", + "$GPGGA,113757.568,4154.963,N,08002.494,W,0,00,0.8,,M,,M,,*71", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.4,0.7*37" +} + +local badGNSSSdata = { + "$GPRMC,113702.568,V,4154.931,N,08002.497,W,95.5,0.02,220721,,E*48", + "$GPGGA,113703.568,4154.931,N,08002.497,W,0,00,,,M,,M,,*54", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.1,0.8,0.6*33", + "$GPRMC,113720.568,V,4154.942,N,08002.498,W,0.2,-0.02,220721,,E*5F", + "$GPGGA,113721.568,4154.942,N,08002.498,W,0,00,0.1,,M,,M,,*82", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.5,0.7*35", + "$GPRMC,113723.568,V,4154.944,N,08002.498,W,53.6,0.05,220721,,E*5A", + "$GPGGA,113724.568,4154.944,N,08002.498,W,0,00,0.5,,M,,M,,*12", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,0.6,0.1*35", + "$GPRMC,113726.568,V,4154.946,N,08002.498,W,76.6,0.04,220721,,E*9E", + "$GPGGA,113727.568,4154.946,N,08002.498,W,0,00,0.6,,M,,M,,*94", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.2,0.3,0.7*32", + "$GPRMC,113729.568,V,4154.948,N,08002.497,W,30.9,0.12,220721,,E*9C", + "$GPGGA,113730.568,4154.948,N,08002.497,W,0,00,0.3,,M,,M,,*79", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.2,0.2*39", + "$GPRMC,113705.568,V,4154.933,N,08002.497,W,86.0,-0.05,220721,,E*90", + "$GPGGA,113706.568,4154.933,N,08002.497,W,0,00,0.8,,M,,M,,*42", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.0,0.4*43", + "$GPRMC,113708.568,V,4154.935,N,08002.498,W,55.1,-0.10,220721,,E*44", + "$GPGGA,113709.568,4154.935,N,08002.498,W,0,00,0.0,,M,,M,,*4A", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.2,0.8*4D", + "$GPRMC,113711.568,V,4154.937,N,08002.498,W,95.0,-0.10,220721,,E*44", + "$GPGGA,113712.568,4154.937,N,08002.498,W,0,00,0.2,,M,,M,,*44", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.7,0.6,0.3*4D", + "$GPRMC,113714.568,V,4154.939,N,08002.498,W,28.0,-0.07,220721,,E*4D", + "$GPGGA,113715.568,4154.939,N,08002.498,W,0,00,0.6,,M,,M,,*4D", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,1.0,1.0,0.5*24", + "$GPRMC,113717.568,V,4154.940,N,08002.498,W,30.1,0.03,220721,,E*59", + "$GPGGA,113718.568,4154.940,N,08002.498,W,0,00,1.0,,M,,M,,*4D", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.5,0.1,0.2*39", + "$GPGGA,113736.568,4154.951,N,08002.496,W,0,00,0.8,,M,,M,,*79", + "$GPGSA,A,3,09,02,08,05,11,15,,,,,,,0.2,0.6,0.1*39", + "$GPRMC,113738.568,V,4154.953,N,08002.496,W,58.2,0.10,220721,,E*67", + "$GPGGA,113739.568,4154.953,N,08002.496,W,0,00,0.6,,M,,M,,*79", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.6,0.9,0.7*35", + "$GPRMC,113741.568,V,4154.955,N,08002.496,W,88.3,0.03,220721,,E*31", + "$GPGGA,113742.568,4154.955,N,08002.496,W,0,00,0.9,,M,,M,,*33", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.8,0.8,0.1*34", + "$GPRMC,113744.568,V,4154.956,N,08002.496,W,89.3,0.10,220721,,E*34", + "$GPGGA,113745.568,4154.956,N,08002.496,W,0,00,0.8,,M,,M,,*75", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.9,0.2,1.0*4B", + "$GPRMC,113747.568,V,4154.958,N,08002.495,W,99.1,0.14,220721,,E*5D", + "$GPGGA,113748.568,4154.958,N,08002.495,W,0,00,0.2,,M,,M,,*5C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.1,0.2*55", + "$GPRMC,113750.568,V,4154.960,N,08002.495,W,84.0,0.19,220721,,E*50", + "$GPGGA,113751.568,4154.960,N,08002.495,W,0,00,0.1,,M,,M,,*5C", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.3,1.0,0.5*35", + "$GPRMC,113753.568,V,4154.962,N,08002.495,W,24.0,0.13,220721,,E*51", + "$GPGGA,113754.568,4154.962,N,08002.495,W,0,00,1.0,,M,,M,,*5B", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.7,0.8,0.9*35", + "$GPRMC,113756.568,V,4154.963,N,08002.494,W,27.8,0.03,220721,,E*5E", + "$GPGGA,113757.568,4154.963,N,08002.494,W,0,00,0.8,,M,,M,,*51", + "$GPGSA,A,2,09,02,08,05,11,15,,,,,,,0.5,0.4,0.7*57" +} + +local GNSStr = "$GPRMC,143753.498,V,3854.930,N,07902.496,W,91.3,0.75,220721,,E*4B\r\n\ + $GPGGA,143754.498,3854.930,N,07902.496,W,0,00,,,M,,M,,*53\r\n\ + $GPGSA,A,2,13,09,13,09,18,16,,,,,,,0.1,0.0,0.2*3E\r\n\ + $GPRMC,143756.498,V,3854.931,N,07902.494,W,92.7,0.76,220721,,E*49\r\n\ + $GPGGA,143757.498,3854.931,N,07902.494,W,0,00,0.0,,M,,M,,*7D\r\n\ + $GPGSA,A,2,13,09,13,09,18,16,,,,,,,0.4,0.4,0.5*38\r\n\ + $GPRMC,143759.498,V,3854.932,N,07902.492,W,15.5,0.75,220721,,E*4D\r\n" + +local badCRC = "$GPRMC,143753.498,V,3854.930,N,07902.496,W,91.3,0.75,220721,,E*6B\r\n\ + $GPGGA,143754.498,3854.930,N,07902.496,W,0,00,,,M,,M,,*43\r\n\ + $GPGSA,A,2,13,09,13,09,18,16,,,,,,,0.1,0.0,0.2*5E\r\n\ + $GPRMC,143756.498,V,3854.931,N,07902.494,W,92.7,0.76,220721,,E*48\r\n\ + $GPGGA,143757.498,3854.931,N,07902.494,W,0,00,0.0,,M,,M,,*4D\r\n\ + $GPGSA,A,2,13,09,13,09,18,16,,,,,,,0.4,0.4,0.5*39\r\n\ + $GPRMC,143759.498,V,3854.932,N,07902.492,W,15.5,0.75,220721,,E*3D\r\n" + +local nmeaDataType = {"$GPGSA,", "$GPRMC,", "$GPGGA,"} +local badString = "oskdsajdij232391i*&^^&7^&^(*&*YDUDHJSBDNBNVyywfdywf" + +function testBitXOR() + lu.assertEquals(BitXOR(string.byte('q'), string.byte('r')), 3) + lu.assertEquals(BitXOR(string.byte('a'), string.byte('b')), 3) + lu.assertEquals(BitXOR(string.byte('1'), string.byte('5')), 4) + lu.assertEquals(BitXOR(string.byte('0'), string.byte('0')), 0) + lu.assertEquals(BitXOR(string.byte('9'), string.byte('1')), 8) + lu.assertEquals(BitXOR(string.byte('f'), string.byte('w')), 17) +end + +function testCRC() + for i = 1, #goodGNSSSdata do + lu.assertEquals(checkCRC(goodGNSSSdata[i]), true) + end + for i = 1, #badGNSSSdata do + lu.assertEquals(checkCRC(badGNSSSdata[i]), false) + end +end + +function testCropData() + for i = 1, #nmeaDataType do + lu.assertEquals(getCropData("", nmeaDataType[i]), {true, "No data found"}) + lu.assertEquals(getCropData(badString, nmeaDataType[i]), {true, "No data found"}) + lu.assertEquals(getCropData(badCRC, nmeaDataType[i]), {true, "Checksum error"}) + lu.assertEquals(getCropData(GNSStr, nmeaDataType[i]), {false, "OK"}) + end +end + +os.exit(lu.LuaUnit.run()) \ No newline at end of file diff --git a/luci-app-gpoint-main/test/nmea_test2.lua b/luci-app-gpoint-main/test/nmea_test2.lua new file mode 100644 index 000000000..fc8121981 --- /dev/null +++ b/luci-app-gpoint-main/test/nmea_test2.lua @@ -0,0 +1,56 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + + +function printData(data, key) + for i, j in pairs(data) do + if i == key then + print("----->",i) + for k, v in pairs(j) do + print(k, v) + print("*********************") + end + end + end + print("----------------------------------------------") +end + +function printError(data) + for i, j in pairs(data) do + print(i) + for k, v in pairs(j) do + if k == "app" then + for m, n in pairs(v) do + print(m, n) + end + end + end + end +end + +local nmea = require("nmea") +local port = "/dev/ttyUSB1" + +-- test get NMEA data (GGA) +--local GGA = nmea.getData("GGA", port) +--printData(GGA, "gga") + +-- test get NMEA data (GNS) +--local GNS = nmea.getData("GNS", port) +--printData(GNS, "gns") + +-- test get RMC data (RMC) +--local RMC = nmea.getData("RMC", port) +--printData(RMC, "rmc") + +-- test get NMEA data (VTG) +--local VTG = nmea.getData("VTG", port) +--printData(VTG, "vtg") + +-- test get NMEA data (GSA) +--local GSA = nmea.getData("GSA", port) +--printData(GSA, "gsa") + +-- test get NMEA data (BAD STRING) +--local AAA = nmea.getData("AAA", port) +--printError(AAA) \ No newline at end of file diff --git a/luci-app-gpoint-main/test/serial_test.lua b/luci-app-gpoint-main/test/serial_test.lua new file mode 100644 index 000000000..8529ff9e1 --- /dev/null +++ b/luci-app-gpoint-main/test/serial_test.lua @@ -0,0 +1,16 @@ +common_path = '/usr/share/gpoint/lib/?.lua;' +package.path = common_path .. package.path + +serial = require("serial") + +function serial_read(port) + local err, data = serial.read(port) + assert(err, data) + print("Error data: OK") + assert(data) + print("Data from serial: OK") +end +local port = "/dev/ttyUSB1" +serial_read(port) + +print("OK") \ No newline at end of file