Bound GPS UTC fields and accept variable time precision
This commit is contained in:
1 parent
135d361368
commit
a16229a3eb
11 files changed
+1934
-18
No files matched your search
@@ -578,6 +578,8 @@ jobs:
|
||||
python3 -B test/test_esp32_gps_discovery.py -v
|
||||
python3 -B test/test_gps_sync_interval.py -v
|
||||
python3 -B test/test_rak_gps_clock_freshness.py -v
|
||||
python3 -B test/test_micronmea_time_fix.py -v
|
||||
python3 -B test/test_micronmea_time_fix_recipes.py -v
|
||||
python3 -B test/test_gps_sync_preferences.py -v
|
||||
python3 -B test/test_rak_gps_power.py -v
|
||||
python3 -B test/test_rak_shutdown_power.py -v
|
||||
|
||||
@@ -85,6 +85,7 @@ monitor_filters = esp32_exception_decoder
|
||||
; it needs a custom bootloader; RP2040/STM32 have no A/B path yet.)
|
||||
extra_scripts =
|
||||
pre:scripts/client_acl_role.py
|
||||
pre:scripts/micronmea_time_fix.py
|
||||
pre:scripts/rtttl_rollover_fix.py
|
||||
pre:scripts/generate_webconfig_html.py
|
||||
pre:scripts/meshcore_image_identity.py
|
||||
@@ -163,6 +164,7 @@ platform_packages =
|
||||
platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
extra_scripts = create-uf2.py
|
||||
pre:scripts/client_acl_role.py
|
||||
pre:scripts/micronmea_time_fix.py
|
||||
pre:scripts/rtttl_rollover_fix.py
|
||||
pre:scripts/ota_device_deflate_role.py
|
||||
pre:scripts/nrf52_usb_power_fix.py
|
||||
@@ -341,6 +343,7 @@ lib_deps = ${nrf52_lora_ota.lib_deps}
|
||||
[rp2040_base]
|
||||
extends = arduino_base
|
||||
extra_scripts = pre:scripts/client_acl_role.py
|
||||
pre:scripts/micronmea_time_fix.py
|
||||
pre:scripts/rtttl_rollover_fix.py
|
||||
post:scripts/check_firmware_ram.py
|
||||
upload_protocol = picotool
|
||||
@@ -362,6 +365,7 @@ extends = arduino_base
|
||||
platform = ststm32@19.5.0
|
||||
extra_scripts = post:arch/stm32/build_hex.py
|
||||
pre:scripts/client_acl_role.py
|
||||
pre:scripts/micronmea_time_fix.py
|
||||
pre:scripts/rtttl_rollover_fix.py
|
||||
post:scripts/check_firmware_ram.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
@@ -475,6 +479,7 @@ lib_deps =
|
||||
[env:native]
|
||||
platform = native
|
||||
test_framework = googletest
|
||||
extra_scripts = pre:scripts/micronmea_time_fix.py
|
||||
; Exercise the real Arduino GPS parser against the native Stream mock.
|
||||
lib_compat_mode = off
|
||||
build_flags = -std=c++17
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Fix reviewed MicroNMEA UTC parsing in private build copies, not libdeps.
|
||||
|
||||
MicroNMEA 2.0.6 skips nine characters for every time field, rejecting valid
|
||||
integer seconds and one-digit fractions. Its fixed-width date/time reads can
|
||||
also pass field boundaries. Scan and validate fields before numeric reads;
|
||||
keep framing, raw sentences, position validity and the public ABI unchanged.
|
||||
The provider still owns calendar validation and fresh-observation admission.
|
||||
"""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SOURCE_SHA256 = "925da47a9aaa75c43c786ffb5a6530544283dda5138cd7792c4e8f79c8238377"
|
||||
HEADER_SHA256 = "d87810b25ff920fe5810b8bf7be17c69eb608853407be9da82214ef5616f1fb2"
|
||||
PATCHED_SHA256 = "df2e660e79235e317ec00b86ec86883655a70a60a38b11ac2eb60189036fc9e0"
|
||||
|
||||
ORIGINAL_TIME = """const char* MicroNMEA::parseTime(const char* s)
|
||||
{
|
||||
\tif (*s == ',')
|
||||
\t\treturn skipField(s);
|
||||
\t_hour = parseUnsignedInt(s, 2);
|
||||
\t_minute = parseUnsignedInt(s + 2, 2);
|
||||
\t_second = parseUnsignedInt(s + 4, 2);
|
||||
\t_hundredths = parseUnsignedInt(s + 7, 2);
|
||||
\treturn skipField(s + 9);
|
||||
}"""
|
||||
|
||||
PATCHED_TIME = """const char* MicroNMEA::parseTime(const char* s)
|
||||
{
|
||||
\t_hour = _minute = _second = 99;
|
||||
\t_hundredths = 0;
|
||||
\tconst char* end = s;
|
||||
\twhile (*end != ',' && !isEndOfFields(*end))
|
||||
\t\t++end;
|
||||
\tif (end - s < 6)
|
||||
\t\treturn skipField(end);
|
||||
\tfor (const char* digit = s; digit < s + 6; ++digit)
|
||||
\t\tif (*digit < '0' || *digit > '9')
|
||||
\t\t\treturn skipField(end);
|
||||
|
||||
\tuint8_t hundredths = 0;
|
||||
\tif (end - s > 6) {
|
||||
\t\tif (s[6] != '.' || end - s < 8)
|
||||
\t\t\treturn skipField(end);
|
||||
\t\tfor (const char* digit = s + 7; digit < end; ++digit) {
|
||||
\t\t\tif (*digit < '0' || *digit > '9')
|
||||
\t\t\t\treturn skipField(end);
|
||||
\t\t\tif (digit == s + 7)
|
||||
\t\t\t\thundredths = (*digit - '0') * 10;
|
||||
\t\t\telse if (digit == s + 8)
|
||||
\t\t\t\thundredths += *digit - '0';
|
||||
\t\t}
|
||||
\t}
|
||||
\t_hour = parseUnsignedInt(s, 2);
|
||||
\t_minute = parseUnsignedInt(s + 2, 2);
|
||||
\t_second = parseUnsignedInt(s + 4, 2);
|
||||
\t_hundredths = hundredths;
|
||||
\treturn skipField(end);
|
||||
}"""
|
||||
|
||||
ORIGINAL_DATE = """const char* MicroNMEA::parseDate(const char* s)
|
||||
{
|
||||
\tif (*s == ',')
|
||||
\t\treturn skipField(s);
|
||||
\t_day = parseUnsignedInt(s, 2);
|
||||
\t_month = parseUnsignedInt(s + 2, 2);
|
||||
\t_year = parseUnsignedInt(s + 4, 2) + 2000;
|
||||
\treturn skipField(s + 6);
|
||||
}"""
|
||||
|
||||
PATCHED_DATE = """const char* MicroNMEA::parseDate(const char* s)
|
||||
{
|
||||
\t_year = _month = _day = 0;
|
||||
\tconst char* end = s;
|
||||
\twhile (*end != ',' && !isEndOfFields(*end))
|
||||
\t\t++end;
|
||||
\tif (end - s != 6)
|
||||
\t\treturn skipField(end);
|
||||
\tfor (const char* digit = s; digit < end; ++digit)
|
||||
\t\tif (*digit < '0' || *digit > '9')
|
||||
\t\t\treturn skipField(end);
|
||||
\t_day = parseUnsignedInt(s, 2);
|
||||
\t_month = parseUnsignedInt(s + 2, 2);
|
||||
\t_year = parseUnsignedInt(s + 4, 2) + 2000;
|
||||
\treturn skipField(end);
|
||||
}"""
|
||||
|
||||
PATCH_NOTICE = (
|
||||
"// MeshCore local changes (2026-10-17): bounded UTC fields and variable-width\n"
|
||||
"// time fractions. Original MicroNMEA 2.0.6 by Steve Marple, GNU LGPL v2.1.\n"
|
||||
)
|
||||
|
||||
|
||||
def digest(source):
|
||||
return hashlib.sha256(source.encode("ascii")).hexdigest()
|
||||
|
||||
|
||||
def patched_source(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
source_hash = digest(source)
|
||||
if source_hash == PATCHED_SHA256:
|
||||
return source
|
||||
if source_hash != SOURCE_SHA256:
|
||||
raise RuntimeError("MicroNMEA UTC fix: changed reviewed source; review dependency update")
|
||||
if (source.count(ORIGINAL_TIME) != 1 or source.count(ORIGINAL_DATE) != 1
|
||||
or source.count("#include <MicroNMEA.h>") != 1):
|
||||
raise RuntimeError("MicroNMEA UTC fix: changed reviewed field parsers")
|
||||
source = PATCH_NOTICE + source.replace("#include <MicroNMEA.h>", '#include "MicroNMEA.h"', 1)
|
||||
source = source.replace(ORIGINAL_TIME, PATCHED_TIME, 1)
|
||||
source = source.replace(ORIGINAL_DATE, PATCHED_DATE, 1)
|
||||
if digest(source) != PATCHED_SHA256:
|
||||
raise RuntimeError("MicroNMEA UTC fix: unexpected transform result")
|
||||
return source
|
||||
|
||||
|
||||
def write_if_changed(path, content):
|
||||
if not path.exists() or path.read_text(encoding="ascii") != content:
|
||||
path.write_text(content, encoding="ascii")
|
||||
|
||||
|
||||
def replace_micronmea_source(build_env, node):
|
||||
source = Path(node.srcnode().get_abspath())
|
||||
if source.name != "MicroNMEA.cpp" or source.parent.name != "src":
|
||||
return node
|
||||
original = source.read_text(encoding="ascii")
|
||||
patched = patched_source(original)
|
||||
try:
|
||||
header = source.with_suffix(".h").read_text(encoding="ascii").replace("\r\n", "\n")
|
||||
except (OSError, UnicodeError) as error:
|
||||
raise RuntimeError("MicroNMEA UTC fix: missing or changed reviewed header") from error
|
||||
if digest(header) != HEADER_SHA256:
|
||||
raise RuntimeError("MicroNMEA UTC fix: changed reviewed header; review dependency update")
|
||||
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-micronmea" / source.name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
# A quoted include binds the source to its reviewed header even if a mock
|
||||
# or unrelated library has placed another MicroNMEA.h earlier in CPPPATH.
|
||||
write_if_changed(destination.with_suffix(".h"), header)
|
||||
write_if_changed(destination, patched)
|
||||
return build_env.File(str(destination))
|
||||
|
||||
|
||||
def install(build_env):
|
||||
build_env.AddBuildMiddleware(replace_micronmea_source, "*MicroNMEA.cpp")
|
||||
|
||||
|
||||
if "Import" in globals():
|
||||
Import("env")
|
||||
install(env)
|
||||
+504
@@ -0,0 +1,504 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
(This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.)
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
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 this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser 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 Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "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
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY 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
|
||||
LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library 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
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random
|
||||
Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
MicroNMEA 2.0.6 regression fixtures
|
||||
=================================
|
||||
The src/MicroNMEA.cpp and src/MicroNMEA.h files are exact original sources from
|
||||
the project's existing stevemarple/MicroNMEA 2.0.6 native dependency.
|
||||
Author: Steve Marple <stevemarple@googlemail.com>
|
||||
Upstream: https://github.com/stevemarple/MicroNMEA
|
||||
License: GNU Lesser General Public License version 2.1, included in LICENSE.
|
||||
Only the license file's trailing blank line was omitted; its text is unchanged.
|
||||
|
||||
Original source SHA-256:
|
||||
925da47a9aaa75c43c786ffb5a6530544283dda5138cd7792c4e8f79c8238377
|
||||
Original header SHA-256:
|
||||
d87810b25ff920fe5810b8bf7be17c69eb608853407be9da82214ef5616f1fb2
|
||||
|
||||
runtime.cpp is MeshCore test code. It compiles the actual complete dependency
|
||||
against the existing portable Arduino/Stream mocks. It does not reproduce a
|
||||
simplified parser. The patched dependency source is generated privately using
|
||||
scripts/micronmea_time_fix.py; the original fixtures remain unchanged.
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
#include <Arduino.h>
|
||||
#include <MicroNMEA.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void require(bool condition, const char* diagnostic) {
|
||||
if (!condition) {
|
||||
std::cerr << "SEMANTIC: " << diagnostic << "\n";
|
||||
std::exit(42);
|
||||
}
|
||||
}
|
||||
|
||||
std::string sentence(const std::string& body) {
|
||||
const std::string line = "$" + body;
|
||||
char checksum[3] = {};
|
||||
MicroNMEA::generateChecksum(line.c_str(), checksum);
|
||||
const std::string result = line + "*" + checksum;
|
||||
require(MicroNMEA::testChecksum(result.c_str()), "fixture checksum");
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string rmc(const std::string& time, const std::string& date = "171026",
|
||||
bool terminal_date = false) {
|
||||
return sentence("GPRMC," + time +
|
||||
",A,4807.038,N,01131.000,E,0.0,0.0," + date +
|
||||
(terminal_date ? "" : ",,,A"));
|
||||
}
|
||||
|
||||
std::string gga(const std::string& time) {
|
||||
return sentence("GPGGA," + time +
|
||||
",4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,");
|
||||
}
|
||||
|
||||
void feed(MicroNMEA& parser, const std::string& raw) {
|
||||
for (char c : raw) parser.process(c);
|
||||
parser.process('\r');
|
||||
}
|
||||
|
||||
void seed(MicroNMEA& parser) {
|
||||
feed(parser, gga("120000.00"));
|
||||
feed(parser, rmc("120000.00"));
|
||||
require(parser.isValid() && parser.getNumSatellites() == 8,
|
||||
"valid fixture seed");
|
||||
require(parser.getYear() == 2026 && parser.getMonth() == 10 &&
|
||||
parser.getDay() == 17, "valid fixture seed date");
|
||||
}
|
||||
|
||||
void checkTime(const MicroNMEA& parser, uint8_t hundredths) {
|
||||
require(parser.getHour() == 12 && parser.getMinute() == 34 &&
|
||||
parser.getSecond() == 56, "parsed UTC time changed");
|
||||
require(parser.getHundredths() == hundredths, "hundredths pad/truncate");
|
||||
}
|
||||
|
||||
void formats() {
|
||||
const std::vector<std::pair<std::string, uint8_t>> inputs = {
|
||||
{"123456", 0}, {"123456.1", 10}, {"123456.12", 12},
|
||||
{"123456.123456", 12}, {"123456.0", 0}, {"123456.09", 9}};
|
||||
for (const auto& input : inputs) {
|
||||
char buffer[100] = {};
|
||||
MicroNMEA parser(buffer, sizeof(buffer));
|
||||
seed(parser);
|
||||
const std::string raw_rmc = rmc(input.first);
|
||||
feed(parser, raw_rmc);
|
||||
require(parser.isValid(), "RMC short time format lost validity");
|
||||
checkTime(parser, input.second);
|
||||
require(parser.getLatitude() == 48117300 &&
|
||||
parser.getLongitude() == 11516666, "RMC location changed");
|
||||
require(std::string(parser.getSentence()) == raw_rmc, "RMC raw changed");
|
||||
const std::string raw_gga = gga(input.first);
|
||||
feed(parser, raw_gga);
|
||||
require(parser.isValid() && parser.getNumSatellites() == 8,
|
||||
"GGA short time format lost validity or satellites");
|
||||
checkTime(parser, input.second);
|
||||
require(std::string(parser.getSentence()) == raw_gga, "GGA raw changed");
|
||||
}
|
||||
}
|
||||
|
||||
void invalidFields() {
|
||||
const std::vector<std::string> dates = {
|
||||
"", "1", "12", "17102", "17102x", "171026x"};
|
||||
for (const auto& date : dates) {
|
||||
char buffer[100] = {};
|
||||
MicroNMEA parser(buffer, sizeof(buffer));
|
||||
seed(parser);
|
||||
const std::string raw = rmc("123456.12", date);
|
||||
feed(parser, raw);
|
||||
require(parser.getYear() == 0 && parser.getMonth() == 0 &&
|
||||
parser.getDay() == 0, "incomplete date retained old UTC date");
|
||||
checkTime(parser, 12);
|
||||
require(parser.isValid() && parser.getNumSatellites() == 8 &&
|
||||
parser.getLatitude() == 48117300,
|
||||
"date rejection destroyed position/satellite state");
|
||||
require(std::string(parser.getSentence()) == raw, "bad-date raw changed");
|
||||
feed(parser, rmc("123456.12"));
|
||||
require(parser.getYear() == 2026 && parser.getMonth() == 10 &&
|
||||
parser.getDay() == 17, "date recovery");
|
||||
}
|
||||
const std::vector<std::string> times = {
|
||||
"", "1", "12", "12345", "12345x", "123456.",
|
||||
"123456.x", "123456.1x"};
|
||||
for (const auto& time : times) {
|
||||
for (bool use_gga : {false, true}) {
|
||||
char buffer[100] = {};
|
||||
MicroNMEA parser(buffer, sizeof(buffer));
|
||||
seed(parser);
|
||||
const std::string raw = use_gga ? gga(time) : rmc(time);
|
||||
feed(parser, raw);
|
||||
require(parser.getHour() == 99 && parser.getMinute() == 99 &&
|
||||
parser.getSecond() == 99 && parser.getHundredths() == 0,
|
||||
"incomplete time retained old UTC time");
|
||||
require(parser.isValid() && parser.getNumSatellites() == 8 &&
|
||||
parser.getLatitude() == 48117300,
|
||||
"time rejection destroyed position/satellite state");
|
||||
require(std::string(parser.getSentence()) == raw, "bad-time raw changed");
|
||||
feed(parser, rmc("123456.12"));
|
||||
checkTime(parser, 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void terminalDate() {
|
||||
char buffer[100] = {};
|
||||
MicroNMEA parser(buffer, sizeof(buffer));
|
||||
seed(parser);
|
||||
const std::string raw = rmc("123456.12", "181026", true);
|
||||
feed(parser, raw);
|
||||
require(parser.isValid() && parser.getYear() == 2026 &&
|
||||
parser.getMonth() == 10 && parser.getDay() == 18,
|
||||
"valid terminal RMC date rejected");
|
||||
require(std::string(parser.getSentence()) == raw, "terminal-date raw changed");
|
||||
}
|
||||
|
||||
unsigned bad_count = 0, unknown_count = 0;
|
||||
void badChecksum(MicroNMEA&) { ++bad_count; }
|
||||
void unknownSentence(MicroNMEA&) { ++unknown_count; }
|
||||
|
||||
class Output : public Stream {
|
||||
public:
|
||||
std::string bytes;
|
||||
size_t write(uint8_t c) override {
|
||||
bytes += static_cast<char>(c);
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
void callbacks() {
|
||||
char buffer[100] = {};
|
||||
MicroNMEA parser(buffer, sizeof(buffer));
|
||||
parser.setBadChecksumHandler(badChecksum);
|
||||
parser.setUnknownSentenceHandler(unknownSentence);
|
||||
seed(parser);
|
||||
std::string bad = rmc("235959.00", "181026");
|
||||
const size_t checksum = bad.find('*') + 1;
|
||||
bad[checksum] = bad[checksum] == '0' ? '1' : '0';
|
||||
require(!MicroNMEA::testChecksum(bad.c_str()), "bad fixture checksum");
|
||||
feed(parser, bad);
|
||||
require(bad_count == 1 && unknown_count == 0, "checksum callback changed");
|
||||
require(parser.getHour() == 12 && parser.getDay() == 17,
|
||||
"bad checksum changed valid state");
|
||||
require(std::string(parser.getSentence()) == bad, "bad-checksum raw changed");
|
||||
parser.process('\n');
|
||||
require(bad_count == 1 && parser.getSentence()[0] == '\0',
|
||||
"CRLF duplicated checksum callback");
|
||||
const std::string other = sentence("GPTXT,01,01,02,unchanged");
|
||||
feed(parser, other);
|
||||
require(unknown_count == 1 && bad_count == 1, "unknown callback changed");
|
||||
require(std::string(parser.getSentence()) == other, "unknown raw changed");
|
||||
require(std::string(parser.getMessageID()) == "TXT" &&
|
||||
parser.getTalkerID() == 'P', "message identifiers changed");
|
||||
parser.process('\n');
|
||||
parser.process('\r');
|
||||
parser.process('\n');
|
||||
require(unknown_count == 1 && bad_count == 1, "empty CRLF callback");
|
||||
Output output;
|
||||
MicroNMEA::sendSentence(output, "$PCAS04,7");
|
||||
require(output.bytes == sentence("PCAS04,7") + "\r\n",
|
||||
"sendSentence framing changed");
|
||||
}
|
||||
|
||||
void boundedBuffer() {
|
||||
std::unique_ptr<char[]> buffer(new char[16]());
|
||||
MicroNMEA parser(buffer.get(), 16);
|
||||
const std::string long_input = "$GPTXT," + std::string(200, 'x') + "*00";
|
||||
feed(parser, long_input);
|
||||
require(std::strlen(parser.getSentence()) == 15,
|
||||
"bounded parser buffer length changed");
|
||||
require(parser.getSentence()[15] == '\0', "buffer terminator missing");
|
||||
parser.process('\n');
|
||||
feed(parser, sentence("GPTXT,x"));
|
||||
require(std::strlen(parser.getSentence()) < 16, "buffer did not recover");
|
||||
}
|
||||
|
||||
void nearBufferDate() {
|
||||
// Date starts at index 95; "1*HH" ends at 98, NUL at 99. Original 2.0.6
|
||||
// unconditionally reads six date digits and crosses the exact heap buffer.
|
||||
std::unique_ptr<char[]> buffer(new char[100]());
|
||||
MicroNMEA parser(buffer.get(), 100);
|
||||
seed(parser);
|
||||
std::string prefix = "$GPRMC,123456.12,A,4807.038,N,01131.000,E,0.0,";
|
||||
require(prefix.size() < 94, "near-buffer fixture prefix");
|
||||
prefix += std::string(94 - prefix.size(), '0');
|
||||
prefix += ',';
|
||||
require(prefix.size() == 95, "near-buffer date offset");
|
||||
const std::string raw = sentence(prefix.substr(1) + "1");
|
||||
require(raw.size() == 99 && raw[95] == '1' && raw[96] == '*',
|
||||
"near-buffer sentence geometry");
|
||||
feed(parser, raw);
|
||||
require(parser.getYear() == 0 && parser.getMonth() == 0 &&
|
||||
parser.getDay() == 0, "near-buffer short date retained UTC");
|
||||
require(parser.isValid() && parser.getLatitude() == 48117300,
|
||||
"near-buffer date rejection destroyed position");
|
||||
require(std::string(parser.getSentence()) == raw, "near-buffer raw changed");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
require(argc == 2, "select one runtime scenario");
|
||||
const std::string mode = argv[1];
|
||||
if (mode == "formats") formats();
|
||||
else if (mode == "invalid") invalidFields();
|
||||
else if (mode == "terminal-date") terminalDate();
|
||||
else if (mode == "callbacks") callbacks();
|
||||
else if (mode == "bounds") boundedBuffer();
|
||||
else if (mode == "near-buffer-date") nearBufferDate();
|
||||
else require(false, "unknown runtime scenario");
|
||||
return 0;
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
#include <MicroNMEA.h>
|
||||
|
||||
// Allow debugging/regression testing under normal g++ environment.
|
||||
#ifdef MICRONMEA_DEBUG
|
||||
#include <stdlib.h>
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
#endif
|
||||
|
||||
|
||||
static long exp10(uint8_t b)
|
||||
{
|
||||
long r = 1;
|
||||
while (b--)
|
||||
r *= 10;
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
static char toHex(uint8_t nibble)
|
||||
{
|
||||
if (nibble >= 10)
|
||||
return nibble + 'A' - 10;
|
||||
else
|
||||
return nibble + '0';
|
||||
|
||||
}
|
||||
|
||||
|
||||
const char* MicroNMEA::skipField(const char* s)
|
||||
{
|
||||
if (s == nullptr)
|
||||
return nullptr;
|
||||
|
||||
while (!isEndOfFields(*s)) {
|
||||
if (*s == ',') {
|
||||
// Check next character
|
||||
if (isEndOfFields(*++s))
|
||||
break;
|
||||
else
|
||||
return s;
|
||||
}
|
||||
++s;
|
||||
}
|
||||
return nullptr; // End of string or valid sentence
|
||||
}
|
||||
|
||||
|
||||
unsigned int MicroNMEA::parseUnsignedInt(const char *s, uint8_t len)
|
||||
{
|
||||
int r = 0;
|
||||
while (len--)
|
||||
r = 10 * r + *s++ - '0';
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
long MicroNMEA::parseFloat(const char* s, uint8_t log10Multiplier, const char** eptr)
|
||||
{
|
||||
int8_t neg = 1;
|
||||
long r = 0;
|
||||
while (isspace(*s))
|
||||
++s;
|
||||
if (*s == '-') {
|
||||
neg = -1;
|
||||
++s;
|
||||
}
|
||||
else if (*s == '+')
|
||||
++s;
|
||||
|
||||
while (isdigit(*s))
|
||||
r = 10*r + *s++ - '0';
|
||||
r *= exp10(log10Multiplier);
|
||||
|
||||
if (*s == '.') {
|
||||
++s;
|
||||
long frac = 0;
|
||||
while (isdigit(*s) && log10Multiplier) {
|
||||
frac = 10 * frac + *s++ -'0';
|
||||
--log10Multiplier;
|
||||
}
|
||||
frac *= exp10(log10Multiplier);
|
||||
r += frac;
|
||||
}
|
||||
r *= neg; // Include effect of any minus sign
|
||||
|
||||
if (eptr)
|
||||
*eptr = skipField(s);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
long MicroNMEA::parseDegreeMinute(const char* s, uint8_t degWidth,
|
||||
const char **eptr)
|
||||
{
|
||||
if (*s == ',') {
|
||||
if (eptr)
|
||||
*eptr = skipField(s);
|
||||
return 0;
|
||||
}
|
||||
long r = parseUnsignedInt(s, degWidth) * 1000000L;
|
||||
s += degWidth;
|
||||
r += parseFloat(s, 6, eptr) / 60;
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
const char* MicroNMEA::parseField(const char* s, char *result, int len)
|
||||
{
|
||||
if (s == nullptr)
|
||||
return nullptr;
|
||||
|
||||
int i = 0;
|
||||
while (*s != ',' && !isEndOfFields(*s)) {
|
||||
if (result && i++ < len)
|
||||
*result++ = *s;
|
||||
++s;
|
||||
}
|
||||
if (result && i < len)
|
||||
*result = '\0'; // Terminate unless too long
|
||||
|
||||
if (*s == ',')
|
||||
return ++s; // Location of start of next field
|
||||
else
|
||||
return nullptr; // End of string or valid sentence
|
||||
}
|
||||
|
||||
|
||||
const char* MicroNMEA::generateChecksum(const char* s, char* checksum)
|
||||
{
|
||||
uint8_t c = 0;
|
||||
// Initial $ is omitted from checksum, if present ignore it.
|
||||
if (*s == '$')
|
||||
++s;
|
||||
|
||||
while (*s != '\0' && *s != '*')
|
||||
c ^= *s++;
|
||||
|
||||
if (checksum) {
|
||||
checksum[0] = toHex(c / 16);
|
||||
checksum[1] = toHex(c % 16);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
bool MicroNMEA::testChecksum(const char* s)
|
||||
{
|
||||
char checksum[2];
|
||||
const char* p = generateChecksum(s, checksum);
|
||||
return *p == '*' && p[1] == checksum[0] && p[2] == checksum[1];
|
||||
}
|
||||
|
||||
|
||||
#ifndef MICRONMEA_DEBUG
|
||||
// When debugging in normal g++ environment ostream doesn't have a
|
||||
// print member function. As sendSentence() isn't needed when
|
||||
// debugging don't compile it.
|
||||
Stream& MicroNMEA::sendSentence(Stream& s, const char* sentence)
|
||||
{
|
||||
char checksum[3];
|
||||
generateChecksum(sentence, checksum);
|
||||
checksum[2] = '\0';
|
||||
s.print(sentence);
|
||||
s.print('*');
|
||||
s.print(checksum);
|
||||
s.print("\r\n");
|
||||
return s;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
MicroNMEA::MicroNMEA(void) :
|
||||
_talkerID('\0'),
|
||||
_messageID{0},
|
||||
_badChecksumHandler(nullptr),
|
||||
_unknownSentenceHandler(nullptr)
|
||||
{
|
||||
setBuffer(nullptr, 0);
|
||||
clear();
|
||||
}
|
||||
|
||||
|
||||
MicroNMEA::MicroNMEA(void* buf, uint8_t len) :
|
||||
_talkerID('\0'),
|
||||
_messageID{0},
|
||||
_badChecksumHandler(nullptr),
|
||||
_unknownSentenceHandler(nullptr)
|
||||
{
|
||||
setBuffer(buf, len);
|
||||
clear();
|
||||
}
|
||||
|
||||
|
||||
void MicroNMEA::setBuffer(void* buf, uint8_t len)
|
||||
{
|
||||
_bufferLen = len;
|
||||
_buffer = (char*)buf;
|
||||
_ptr = _buffer;
|
||||
if (_bufferLen) {
|
||||
*_ptr = '\0';
|
||||
_buffer[_bufferLen - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MicroNMEA::clear(void)
|
||||
{
|
||||
_navSystem = '\0';
|
||||
_numSat = 0;
|
||||
_hdop = 255;
|
||||
_isValid = false;
|
||||
_latitude = 999000000L;
|
||||
_longitude = 999000000L;
|
||||
_altitude = _speed = _course = LONG_MIN;
|
||||
_altitudeValid = false;
|
||||
_year = _month = _day = 0;
|
||||
_hour = _minute = _second = 99;
|
||||
_hundredths = 0;
|
||||
}
|
||||
|
||||
|
||||
bool MicroNMEA::process(char c)
|
||||
{
|
||||
if (_buffer == nullptr || _bufferLen == 0)
|
||||
return false;
|
||||
if (c == '\0' || c == '\n' || c == '\r') {
|
||||
// Terminate buffer then reset pointer
|
||||
*_ptr = '\0';
|
||||
_ptr = _buffer;
|
||||
|
||||
if (*_buffer == '$' && testChecksum(_buffer)) {
|
||||
// Valid message
|
||||
const char* data;
|
||||
if (_buffer[1] == 'G') {
|
||||
_talkerID = _buffer[2];
|
||||
data = parseField(&_buffer[3], &_messageID[0], sizeof(_messageID));
|
||||
}
|
||||
else {
|
||||
_talkerID = '\0';
|
||||
data = parseField(&_buffer[1], &_messageID[0], sizeof(_messageID));
|
||||
}
|
||||
|
||||
if (data != nullptr && strcmp(&_messageID[0], "GGA") == 0)
|
||||
return processGGA(data);
|
||||
else if (data != nullptr && strcmp(&_messageID[0], "RMC") == 0)
|
||||
return processRMC(data);
|
||||
else if (_unknownSentenceHandler)
|
||||
(*_unknownSentenceHandler)(*this);
|
||||
}
|
||||
else {
|
||||
if (_badChecksumHandler && *_buffer != '\0') // don't send empty buffers as bad checksums!
|
||||
(*_badChecksumHandler)(*this);
|
||||
}
|
||||
// Return true for a complete, non-empty, sentence (even if not a valid one).
|
||||
return *_buffer != '\0'; //
|
||||
}
|
||||
else {
|
||||
*_ptr = c;
|
||||
if (_ptr < &_buffer[_bufferLen - 1])
|
||||
++_ptr;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const char* MicroNMEA::parseTime(const char* s)
|
||||
{
|
||||
if (*s == ',')
|
||||
return skipField(s);
|
||||
_hour = parseUnsignedInt(s, 2);
|
||||
_minute = parseUnsignedInt(s + 2, 2);
|
||||
_second = parseUnsignedInt(s + 4, 2);
|
||||
_hundredths = parseUnsignedInt(s + 7, 2);
|
||||
return skipField(s + 9);
|
||||
}
|
||||
|
||||
|
||||
const char* MicroNMEA::parseDate(const char* s)
|
||||
{
|
||||
if (*s == ',')
|
||||
return skipField(s);
|
||||
_day = parseUnsignedInt(s, 2);
|
||||
_month = parseUnsignedInt(s + 2, 2);
|
||||
_year = parseUnsignedInt(s + 4, 2) + 2000;
|
||||
return skipField(s + 6);
|
||||
}
|
||||
|
||||
|
||||
bool MicroNMEA::processGGA(const char *s)
|
||||
{
|
||||
// If GxGSV messages are received _talker_ID can be changed after
|
||||
// other MicroNMEA sentences. Compatibility modes can set the talker ID
|
||||
// to indicate GPS regardless of actual navigation system used.
|
||||
_navSystem = _talkerID;
|
||||
|
||||
s = parseTime(s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
// ++s;
|
||||
_latitude = parseDegreeMinute(s, 2, &s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
if (*s == ',')
|
||||
++s;
|
||||
else {
|
||||
if (*s == 'S')
|
||||
_latitude *= -1;
|
||||
s += 2; // Skip N/S and comma
|
||||
}
|
||||
_longitude = parseDegreeMinute(s, 3, &s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
if (*s == ',')
|
||||
++s;
|
||||
else {
|
||||
if (*s == 'W')
|
||||
_longitude *= -1;
|
||||
s += 2; // Skip E/W and comma
|
||||
}
|
||||
_isValid = (*s >= '1' && *s <= '5');
|
||||
s += 2; // Skip position fix flag and comma
|
||||
long tmp = parseFloat(s, 0, &s);
|
||||
_numSat = (tmp > 255 ? 255 : (tmp < 0 ? 0 : tmp));
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
tmp = parseFloat(s, 1, &s);
|
||||
_hdop = (tmp > 255 || tmp < 0 ? 255 : tmp);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
_altitude = parseFloat(s, 3, &s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
_altitudeValid = true;
|
||||
// That's all we care about
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool MicroNMEA::processRMC(const char* s)
|
||||
{
|
||||
// If GxGSV messages are received _talker_ID can be changed after
|
||||
// other MicroNMEA sentences. Compatibility modes can set the talker
|
||||
// ID to indicate GPS regardless of actual navigation system used.
|
||||
_navSystem = _talkerID;
|
||||
|
||||
s = parseTime(s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
_isValid = (*s == 'A');
|
||||
s += 2; // Skip validity and comma
|
||||
_latitude = parseDegreeMinute(s, 2, &s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
if (*s == ',')
|
||||
++s;
|
||||
else {
|
||||
if (*s == 'S')
|
||||
_latitude *= -1;
|
||||
s += 2; // Skip N/S and comma
|
||||
}
|
||||
_longitude = parseDegreeMinute(s, 3, &s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
if (*s == ',')
|
||||
++s;
|
||||
else {
|
||||
if (*s == 'W')
|
||||
_longitude *= -1;
|
||||
s += 2; // Skip E/W and comma
|
||||
}
|
||||
_speed = parseFloat(s, 3, &s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
_course = parseFloat(s, 3, &s);
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
s = parseDate(s);
|
||||
// That's all we care about
|
||||
return true;
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
#ifndef MICRONMEA_H
|
||||
#define MICRONMEA_H
|
||||
|
||||
#define MICRONMEA_VERSION "2.0.6"
|
||||
#include <limits.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
/**
|
||||
* @file MicroNMEA.h
|
||||
* @author Steve Marple
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @class MicroNMEA
|
||||
* @brief Process MicroNMEA sentences from GPS and GNSS receivers.
|
||||
* @details The user is responsible to allocating the buffer that MicroNMEA uses. This
|
||||
* enables a static buffer to be used if desired so that `malloc()` is not required.
|
||||
* Values returned are integers, floating-point maths is not used.
|
||||
*/
|
||||
class MicroNMEA {
|
||||
public:
|
||||
|
||||
static const char* skipField(const char* s);
|
||||
static unsigned int parseUnsignedInt(const char* s, uint8_t len);
|
||||
static long parseFloat(const char* s, uint8_t log10Multiplier,
|
||||
const char** eptr = nullptr);
|
||||
static long parseDegreeMinute(const char* s, uint8_t degWidth,
|
||||
const char** eptr = nullptr);
|
||||
static const char* parseToComma(const char* s, char *result = nullptr,
|
||||
int len = 0);
|
||||
static const char* parseField(const char* s, char *result = nullptr,
|
||||
int len = 0);
|
||||
static const char* generateChecksum(const char* s, char* checksum);
|
||||
static bool testChecksum(const char* s);
|
||||
|
||||
/**
|
||||
* @brief Send a NMEA sentence to the GNSS receiver.
|
||||
*
|
||||
* @param s Stream to which the GNSS receiver is connected
|
||||
* @param sentence The NMEA sentence to send
|
||||
* @details The sentence must start with `$`; the checksum
|
||||
* and `\r\n` terminators will be appended automatically.
|
||||
* @return The GNSS stream
|
||||
*/
|
||||
static Stream& sendSentence(Stream &s, const char* sentence);
|
||||
|
||||
/**
|
||||
* @brief Default constructor
|
||||
* @details User **must** call setrBuffer() before use
|
||||
*/
|
||||
MicroNMEA(void);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Construct object and pass in the buffer allocated for MicroNMEA to use
|
||||
*/
|
||||
MicroNMEA(void* buffer, uint8_t len);
|
||||
|
||||
/**
|
||||
* @brief Set the buffer object
|
||||
*
|
||||
* @param buf Address of the buffer
|
||||
* @param len Number of bytes allocated
|
||||
*/
|
||||
void setBuffer(void* buf, uint8_t len);
|
||||
|
||||
// Clear all fix information. isValid() will return false, Year,
|
||||
// month and day will all be zero. Hour, minute and second time will
|
||||
// be set to 99. Speed, course and altitude will be set to
|
||||
// LONG_MIN; the altitude validity flag will be false. Latitude and
|
||||
// longitude will be set to 999 degrees.
|
||||
/**
|
||||
* @brief Clear all fix information
|
||||
* @details `isValid()` will return false, year,
|
||||
* month and day will all be zero. Hour, minute and second will
|
||||
* be set to 99. Speed, course and altitude will be set to
|
||||
* `LONG_MIN`; the altitude validity flag will be false. Latitude and
|
||||
* longitude will be set to 999 degrees.
|
||||
*/
|
||||
void clear(void);
|
||||
|
||||
/**
|
||||
* @brief Get the navigation system in use
|
||||
* @details `N` = GNSS, `P` = GPS, `L` = GLONASS, `A` = Galileo, `\0` = none
|
||||
* @return char
|
||||
*/
|
||||
char getNavSystem(void) const {
|
||||
return _navSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the number of satellites in use
|
||||
*
|
||||
* @return uint8_t
|
||||
*/
|
||||
uint8_t getNumSatellites(void) const {
|
||||
return _numSat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the horizontal dilution of precision (HDOP), in tenths
|
||||
* @details A HDOP value of 1.1 is returned as `11`
|
||||
* @return uint8_t
|
||||
*/
|
||||
uint8_t getHDOP(void) const {
|
||||
return _hdop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Inquire if latest fix is valid
|
||||
*
|
||||
* @return true Valid
|
||||
* @return false Not valid
|
||||
*/
|
||||
bool isValid(void) const {
|
||||
return _isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the latitude, in millionths of a degree
|
||||
* @details North is positive.
|
||||
* @return long
|
||||
*/
|
||||
long getLatitude(void) const {
|
||||
return _latitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the longitude, in millionths of a degree
|
||||
* @details East is positive.
|
||||
* @return long
|
||||
*/
|
||||
long getLongitude(void) const {
|
||||
return _longitude;
|
||||
}
|
||||
|
||||
// Altitude in millimetres.
|
||||
/**
|
||||
* @brief Get the altitude in millmetres
|
||||
*
|
||||
* @param alt Reference to long value where altitude is to be stored
|
||||
* @return true Altitude is valid
|
||||
* @return false Altitude not valid
|
||||
*/
|
||||
bool getAltitude(long &alt) const {
|
||||
if (_altitudeValid)
|
||||
alt = _altitude;
|
||||
return _altitudeValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the year
|
||||
*
|
||||
* @return uint16_t year
|
||||
*/
|
||||
uint16_t getYear(void) const {
|
||||
return _year;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the month (1 - 12 inclusive)
|
||||
*
|
||||
* @return uint8_t year
|
||||
*/
|
||||
uint8_t getMonth(void) const {
|
||||
return _month;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the day of month (1 - 31 inclusive)
|
||||
*
|
||||
* @return uint8_t month
|
||||
*/
|
||||
uint8_t getDay(void) const {
|
||||
return _day;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the hour
|
||||
*
|
||||
* @return uint8_t hour
|
||||
*/
|
||||
uint8_t getHour(void) const {
|
||||
return _hour;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the minute
|
||||
*
|
||||
* @return uint8_t minute
|
||||
*/
|
||||
uint8_t getMinute(void) const {
|
||||
return _minute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the integer part of the second
|
||||
*
|
||||
* @return uint8_t second
|
||||
*/
|
||||
uint8_t getSecond(void) const {
|
||||
return _second;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the hundredths part of the second
|
||||
*
|
||||
* @return uint8_t hundredths
|
||||
*/
|
||||
uint8_t getHundredths(void) const {
|
||||
return _hundredths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the speed
|
||||
*
|
||||
* @return uint8_t speed
|
||||
*/
|
||||
long getSpeed(void) const {
|
||||
return _speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the direction of travel
|
||||
* @return Direction in thousandths of a degree, clockwise from North
|
||||
*/
|
||||
long getCourse(void) const {
|
||||
return _course;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Instruct MicroNMEA to process a character
|
||||
*
|
||||
* @param c Character to process
|
||||
* @return true A complete non-empty sentence has been processed (may not be valid)
|
||||
* @return false End of sentence not detected
|
||||
*/
|
||||
bool process(char c);
|
||||
|
||||
/**
|
||||
* @brief Register a handler to be called when bad checksums are detected
|
||||
*
|
||||
* @param handler pointer to handler function
|
||||
*/
|
||||
void setBadChecksumHandler(void (*handler)(MicroNMEA& nmea)) {
|
||||
_badChecksumHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Register a handler to be called when an unknown NMEA sentence is detected
|
||||
*
|
||||
* @param handler pointer to handler function
|
||||
*/
|
||||
void setUnknownSentenceHandler(void (*handler)(MicroNMEA& nmea)) {
|
||||
_unknownSentenceHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get NMEA sentence
|
||||
*
|
||||
* @return const char*
|
||||
*/
|
||||
const char* getSentence(void) const {
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
// Talker ID for current MicroNMEA sentence
|
||||
char getTalkerID(void) const {
|
||||
return _talkerID;
|
||||
}
|
||||
|
||||
// Message ID for current MicroNMEA sentence
|
||||
const char* getMessageID(void) const {
|
||||
return (const char*)_messageID;
|
||||
}
|
||||
|
||||
|
||||
protected:
|
||||
static inline bool isEndOfFields(char c) {
|
||||
return c == '*' || c == '\0' || c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
const char* parseTime(const char* s);
|
||||
const char* parseDate(const char* s);
|
||||
|
||||
bool processGGA(const char *s);
|
||||
bool processRMC(const char* s);
|
||||
|
||||
private:
|
||||
// Sentence buffer and associated pointers
|
||||
// static const uint8_t _bufferLen = 83; // 82 + NULL
|
||||
// char _buffer[_bufferLen];
|
||||
uint8_t _bufferLen;
|
||||
char* _buffer;
|
||||
char *_ptr;
|
||||
|
||||
// Information from current MicroNMEA sentence
|
||||
char _talkerID;
|
||||
char _messageID[6];
|
||||
|
||||
// Variables parsed and kept for user
|
||||
char _navSystem;
|
||||
bool _isValid;
|
||||
long _latitude, _longitude; // In millionths of a degree
|
||||
long _altitude; // In millimetres
|
||||
bool _altitudeValid;
|
||||
long _speed, _course;
|
||||
uint16_t _year;
|
||||
uint8_t _month, _day, _hour, _minute, _second, _hundredths;
|
||||
uint8_t _numSat;
|
||||
uint8_t _hdop;
|
||||
|
||||
void (*_badChecksumHandler)(MicroNMEA &nmea);
|
||||
void (*_unknownSentenceHandler)(MicroNMEA &nmea);
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
@@ -462,8 +462,9 @@ TEST(GpsClockFreshness, PartialOrNonNumericRmcClockFieldsCannotCount) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GpsClockFreshness, CompleteRmcTimeAllowsSupportedDecimalSeconds) {
|
||||
for (const char* time : {"120002.00", "120002.000", "120002.123456"}) {
|
||||
TEST(GpsClockFreshness, CompleteRmcTimeAllowsIntegerAndDecimalSeconds) {
|
||||
for (const char* time : {"120002", "120002.0", "120002.00",
|
||||
"120002.000", "120002.123456"}) {
|
||||
SCOPED_TRACE(time);
|
||||
GpsRig rig;
|
||||
rig.window(pair("120000.00"));
|
||||
@@ -475,25 +476,22 @@ TEST(GpsClockFreshness, CompleteRmcTimeAllowsSupportedDecimalSeconds) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GpsClockFreshness, PinnedParserShortTimeFractionsAreRejectedThenDecimalRecovers) {
|
||||
for (const char* time : {"120002", "120002.0"}) {
|
||||
SCOPED_TRACE(time);
|
||||
TEST(GpsClockFreshness, GgaAndRmcTimePrecisionPreservesFixAndClockAcquisition) {
|
||||
for (const char* fraction : {"", ".0", ".00", ".000", ".123456"}) {
|
||||
SCOPED_TRACE(fraction);
|
||||
GpsRig rig;
|
||||
rig.window(pair("120000.00"));
|
||||
rig.window(pair("120001.00"));
|
||||
// MicroNMEA 2.0.6 skips a fixed nine time characters before seeking the
|
||||
// next field. No fraction or one fractional digit is not a provider fix.
|
||||
// Reject these formats rather than reusing retained clock data; accepting
|
||||
// them requires a separate parser/location compatibility fix.
|
||||
rig.window(gga("120002.00") + rmc(time));
|
||||
EXPECT_FALSE(rig.gps.isValid());
|
||||
rig.window(pair(std::string("120000") + fraction));
|
||||
ASSERT_TRUE(rig.gps.isValid());
|
||||
EXPECT_EQ(8, rig.gps.satellitesCount());
|
||||
EXPECT_EQ(48117300L, rig.gps.getLatitude());
|
||||
EXPECT_EQ(11516666L, rig.gps.getLongitude());
|
||||
EXPECT_EQ(545400L, rig.gps.getAltitude());
|
||||
rig.window(pair(std::string("120001") + fraction));
|
||||
EXPECT_TRUE(rig.rtc.writes.empty());
|
||||
rig.window(pair("120003.00"));
|
||||
rig.window(pair("120004.00"));
|
||||
EXPECT_TRUE(rig.rtc.writes.empty());
|
||||
rig.window(pair("120005.00"));
|
||||
rig.window(pair(std::string("120002") + fraction));
|
||||
ASSERT_EQ(1U, rig.rtc.writes.size());
|
||||
EXPECT_EQ(NOON_2026_10_17 + 5, rig.rtc.writes.back().after);
|
||||
EXPECT_EQ(NOON_2026_10_17 + 2, rig.rtc.writes.back().after);
|
||||
EXPECT_EQ(1U, rig.gps.getTimeSyncGeneration());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
"""Exercise the reviewed complete GPS parser and its private build transform.
|
||||
|
||||
The compiler uses the real MicroNMEA fixture with Arduino/Stream adapters.
|
||||
No PlatformIO command, dependency download, serial port or hardware is used.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURES = ROOT / "test/fixtures/micronmea_time_fix"
|
||||
SPEC = importlib.util.spec_from_file_location("micronmea_time_fix", ROOT / "scripts/micronmea_time_fix.py")
|
||||
FIX = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(FIX)
|
||||
SOURCE = (FIXTURES / "src/MicroNMEA.cpp").read_text(encoding="ascii")
|
||||
HEADER = (FIXTURES / "src/MicroNMEA.h").read_text(encoding="ascii")
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, path):
|
||||
self.path = Path(path)
|
||||
|
||||
def srcnode(self):
|
||||
return self
|
||||
|
||||
def get_abspath(self):
|
||||
return str(self.path)
|
||||
|
||||
|
||||
class BuildEnv:
|
||||
def __init__(self, build):
|
||||
self.build = Path(build)
|
||||
self.registrations = []
|
||||
|
||||
def subst(self, value):
|
||||
assert value == "$BUILD_DIR"
|
||||
return str(self.build)
|
||||
|
||||
def File(self, path):
|
||||
return Path(path)
|
||||
|
||||
def AddBuildMiddleware(self, callback, pattern):
|
||||
self.registrations.append((callback, pattern))
|
||||
|
||||
|
||||
class TransformTests(unittest.TestCase):
|
||||
def test_original_fixtures_are_exact_ascii_reviewed_and_attributed(self):
|
||||
for path in FIXTURES.rglob("*"):
|
||||
if path.is_file():
|
||||
path.read_text(encoding="ascii")
|
||||
self.assertEqual(FIX.digest(SOURCE), FIX.SOURCE_SHA256)
|
||||
self.assertEqual(FIX.digest(HEADER), FIX.HEADER_SHA256)
|
||||
self.assertIn('#define MICRONMEA_VERSION "2.0.6"', HEADER)
|
||||
self.assertIn("Steve Marple", (FIXTURES / "README.txt").read_text())
|
||||
license_text = (FIXTURES / "LICENSE").read_text(encoding="ascii")
|
||||
self.assertIn("GNU LESSER GENERAL PUBLIC LICENSE", license_text)
|
||||
self.assertIn("Version 2.1, February 1999", license_text)
|
||||
|
||||
def test_transform_is_hash_bound_and_idempotent_with_lf_or_crlf(self):
|
||||
patched = FIX.patched_source(SOURCE)
|
||||
self.assertEqual(FIX.digest(patched), FIX.PATCHED_SHA256)
|
||||
self.assertEqual(FIX.patched_source(SOURCE.replace("\n", "\r\n")), patched)
|
||||
self.assertEqual(FIX.patched_source(patched), patched)
|
||||
self.assertEqual(FIX.patched_source(patched.replace("\n", "\r\n")), patched)
|
||||
self.assertIn(FIX.PATCH_NOTICE, patched)
|
||||
self.assertEqual(patched.count(FIX.PATCHED_TIME), 1)
|
||||
self.assertEqual(patched.count(FIX.PATCHED_DATE), 1)
|
||||
|
||||
def test_unknown_changed_and_half_patched_sources_fail_closed(self):
|
||||
for source in (SOURCE + "\n", SOURCE.replace("return r;", "return r + 1;", 1),
|
||||
SOURCE.replace(FIX.ORIGINAL_TIME, FIX.PATCHED_TIME, 1),
|
||||
SOURCE.replace(FIX.ORIGINAL_DATE, FIX.PATCHED_DATE, 1)):
|
||||
with self.subTest(source=FIX.digest(source)):
|
||||
with self.assertRaisesRegex(RuntimeError, "changed reviewed source"):
|
||||
FIX.patched_source(source)
|
||||
|
||||
def test_middleware_matches_only_the_expected_library_source(self):
|
||||
env = BuildEnv("unused")
|
||||
FIX.install(env)
|
||||
self.assertEqual(env.registrations, [(FIX.replace_micronmea_source, "*MicroNMEA.cpp")])
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
env = BuildEnv(Path(temporary) / "build")
|
||||
for relative in ("src/unrelated.cpp", "other/MicroNMEA.cpp"):
|
||||
node = Node(Path(temporary) / relative)
|
||||
self.assertIs(FIX.replace_micronmea_source(env, node), node)
|
||||
self.assertFalse(env.build.exists())
|
||||
|
||||
def test_private_pair_leaves_downloaded_files_and_unchanged_outputs_untouched(self):
|
||||
for crlf in (False, True):
|
||||
with self.subTest(crlf=crlf), tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "libdeps/MicroNMEA/src/MicroNMEA.cpp"
|
||||
source.parent.mkdir(parents=True)
|
||||
original = SOURCE.replace("\n", "\r\n") if crlf else SOURCE
|
||||
header = HEADER.replace("\n", "\r\n") if crlf else HEADER
|
||||
source.write_bytes(original.encode("ascii"))
|
||||
source.with_suffix(".h").write_bytes(header.encode("ascii"))
|
||||
before = source.read_bytes(), source.with_suffix(".h").read_bytes()
|
||||
env = BuildEnv(root / "build")
|
||||
destination = FIX.replace_micronmea_source(env, Node(source))
|
||||
self.assertEqual(destination, env.build / "patched-micronmea/MicroNMEA.cpp")
|
||||
self.assertEqual(destination.read_text(encoding="ascii"), FIX.patched_source(SOURCE))
|
||||
self.assertEqual(destination.with_suffix(".h").read_text(encoding="ascii"), HEADER)
|
||||
times = destination.stat().st_mtime_ns, destination.with_suffix(".h").stat().st_mtime_ns
|
||||
self.assertEqual(FIX.replace_micronmea_source(env, Node(source)), destination)
|
||||
self.assertEqual(times, (destination.stat().st_mtime_ns,
|
||||
destination.with_suffix(".h").stat().st_mtime_ns))
|
||||
self.assertEqual(before, (source.read_bytes(), source.with_suffix(".h").read_bytes()))
|
||||
|
||||
def test_missing_or_changed_header_fails_before_creating_private_output(self):
|
||||
for header in (None, HEADER + "\n", "// unknown\n", "\u00b0"):
|
||||
with self.subTest(missing=header is None), tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "libdeps/MicroNMEA/src/MicroNMEA.cpp"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text(SOURCE, encoding="ascii")
|
||||
if header is not None:
|
||||
source.with_suffix(".h").write_text(header, encoding="utf-8")
|
||||
env = BuildEnv(root / "build")
|
||||
with self.assertRaisesRegex(RuntimeError, "reviewed header"):
|
||||
FIX.replace_micronmea_source(env, Node(source))
|
||||
self.assertFalse(env.build.exists())
|
||||
|
||||
|
||||
class RuntimeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if not compiler:
|
||||
raise unittest.SkipTest("a host C++17 compiler is required")
|
||||
temporary = tempfile.TemporaryDirectory(prefix="micronmea-time-")
|
||||
cls.addClassCleanup(temporary.cleanup)
|
||||
root = Path(temporary.name)
|
||||
cls.binaries = {}
|
||||
sanitize = [] if not sys.platform.startswith("linux") else [
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
|
||||
for kind in ("original", "patched"):
|
||||
source = root / kind / "src/MicroNMEA.cpp"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text(SOURCE, encoding="ascii")
|
||||
source.with_suffix(".h").write_text(HEADER, encoding="ascii")
|
||||
if kind == "patched":
|
||||
source = FIX.replace_micronmea_source(BuildEnv(root / "private"), Node(source))
|
||||
binary = root / (kind + ".exe")
|
||||
built = subprocess.run([
|
||||
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", *sanitize, "-I" + str(source.parent),
|
||||
"-isystem", str(ROOT / "test/mocks"), str(FIXTURES / "runtime.cpp"),
|
||||
str(source), "-o", str(binary)], capture_output=True, text=True, timeout=60)
|
||||
if built.returncode:
|
||||
raise AssertionError(built.stdout + built.stderr)
|
||||
cls.binaries[kind] = binary
|
||||
# Force a wrong header ahead of the reviewed private pair. Only the
|
||||
# library implementation is compiled here; its quoted include must win.
|
||||
poison = root / "poison"
|
||||
poison.mkdir()
|
||||
(poison / "MicroNMEA.h").write_text('#error "unpaired header"\n', encoding="ascii")
|
||||
checked = subprocess.run([
|
||||
compiler, "-std=c++17", "-I" + str(poison), "-I" + str(ROOT / "test/mocks"),
|
||||
"-c", str(root / "private/patched-micronmea/MicroNMEA.cpp"),
|
||||
"-o", str(root / "private.o")], capture_output=True, text=True, timeout=30)
|
||||
if checked.returncode:
|
||||
raise AssertionError(checked.stdout + checked.stderr)
|
||||
|
||||
def run_case(self, scenario, kind="patched", diagnostic=None):
|
||||
result = subprocess.run([str(self.binaries[kind]), scenario],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if diagnostic:
|
||||
self.assertEqual(result.returncode, 42, result.stdout + result.stderr)
|
||||
self.assertIn("SEMANTIC: " + diagnostic, result.stderr)
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertNotIn("AddressSanitizer:", result.stderr)
|
||||
self.assertNotIn("runtime error:", result.stderr)
|
||||
|
||||
def test_integer_and_variable_fraction_times_preserve_real_position_and_raw_sentences(self):
|
||||
self.run_case("formats")
|
||||
|
||||
def test_malformed_clock_fields_clear_utc_but_preserve_valid_position_and_recover(self):
|
||||
self.run_case("invalid")
|
||||
|
||||
def test_terminal_date_without_trailing_optional_fields_is_still_valid(self):
|
||||
self.run_case("terminal-date")
|
||||
|
||||
def test_bad_checksum_unknown_sentence_crlf_and_send_callbacks_are_unchanged(self):
|
||||
self.run_case("callbacks")
|
||||
self.run_case("callbacks", "original")
|
||||
|
||||
def test_small_buffer_and_end_of_buffer_short_date_stay_in_bounds(self):
|
||||
self.run_case("bounds")
|
||||
self.run_case("near-buffer-date")
|
||||
|
||||
def test_original_parser_fails_semantic_compatibility_and_stale_utc_oracles(self):
|
||||
self.run_case("formats", "original", "RMC short time format lost validity")
|
||||
self.run_case("invalid", "original", "incomplete date retained old UTC date")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Resolve tracked GPS parser hooks, including real Full target synthesis.
|
||||
|
||||
Only PlatformIO's configuration reader and the builder's JSON target mapper
|
||||
run here. No PlatformIO CLI, dependency download, compilation or flashing.
|
||||
Developer-local overlays are deliberately excluded from the tracked matrix.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from platformio.project.config import ProjectConfig
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HOOK = 'pre:scripts/micronmea_time_fix.py'
|
||||
|
||||
|
||||
class MicroNmeaTimeFixRecipeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.config = ProjectConfig(str(ROOT / 'platformio.ini'), parse_extra=False)
|
||||
for path in sorted((ROOT / 'variants').glob('*/platformio.ini')):
|
||||
cls.config.read(str(path), parse_extra=False)
|
||||
|
||||
def has_micronmea(self, section):
|
||||
return any('MicroNMEA' in dependency
|
||||
for dependency in self.config.get(section, 'lib_deps', []))
|
||||
|
||||
def test_every_resolved_micronmea_recipe_loads_exactly_one_fix(self):
|
||||
tested = []
|
||||
for section in self.config.sections():
|
||||
if not section.startswith('env:') or not self.has_micronmea(section):
|
||||
continue
|
||||
with self.subTest(target=section):
|
||||
self.assertEqual(self.config.get(section, 'extra_scripts', []).count(HOOK), 1)
|
||||
tested.append(section)
|
||||
self.assertGreater(len(tested), 500)
|
||||
for target in ('native', 'RAK_4631_sensor', 't1000e_sensor', 'heltec_v4_sensor',
|
||||
't1000e_companion_radio_usb', 'heltec_v4_r8_tft_companion_radio_ble'):
|
||||
self.assertIn('env:' + target, tested)
|
||||
|
||||
def test_all_four_platform_bases_and_native_propagate_exactly_one_fix(self):
|
||||
for section in ('esp32_base', 'nrf52_base', 'rp2040_base', 'stm32_base', 'env:native'):
|
||||
with self.subTest(section=section):
|
||||
self.assertEqual(self.config.get(section, 'extra_scripts', []).count(HOOK), 1)
|
||||
|
||||
def test_native_compiles_the_reviewed_real_parser(self):
|
||||
self.assertIn('stevemarple/MicroNMEA @ 2.0.6',
|
||||
self.config.get('env:native', 'lib_deps'))
|
||||
self.assertEqual(self.config.get('env:native', 'lib_compat_mode'), 'off')
|
||||
|
||||
def test_actual_builder_synthesized_full_targets_retain_their_base_hook(self):
|
||||
options = ['build_flags', 'lib_deps', 'build_src_filter', 'board', 'extra_scripts']
|
||||
records = [[section, [[key, self.config.get(section, key, [])] for key in options]]
|
||||
for section in self.config.sections() if section.startswith('env:')]
|
||||
shell = r'''
|
||||
source "$1/build_legacy.sh"
|
||||
pio() { printf 'FORBIDDEN_PLATFORMIO\n' >&2; return 99; }
|
||||
platformio() { printf 'FORBIDDEN_PLATFORMIO\n' >&2; return 99; }
|
||||
ALL_PIO_ENVS=(fixture)
|
||||
PIO_CONFIG_JSON=$(<"$2")
|
||||
init_project_context
|
||||
for target in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if is_companion_radio_full_target "$target"; then
|
||||
printf '%s\t%s\n' "$target" "${PIO_ENV_BUILD_BASE_BY_NAME[$target]:-$target}"
|
||||
fi
|
||||
done
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='micronmea-recipes-') as temporary:
|
||||
path = Path(temporary) / 'config.json'
|
||||
path.write_text(json.dumps(records, ensure_ascii=True), encoding='ascii')
|
||||
result = subprocess.run([
|
||||
'bash', '-c', shell, 'micronmea-recipes', str(ROOT), str(path)],
|
||||
cwd=ROOT, capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertNotIn('FORBIDDEN_PLATFORMIO', result.stdout + result.stderr)
|
||||
pairs = dict(line.split('\t') for line in result.stdout.splitlines())
|
||||
self.assertIn('t1000e_companion_radio_full', pairs)
|
||||
self.assertIn('heltec_v4_r8_tft_companion_radio_full', pairs)
|
||||
tested = []
|
||||
for target, base in pairs.items():
|
||||
section = 'env:' + base
|
||||
if not self.has_micronmea(section):
|
||||
continue
|
||||
with self.subTest(target=target, base=base):
|
||||
self.assertEqual(self.config.get(section, 'extra_scripts', []).count(HOOK), 1)
|
||||
tested.append(target)
|
||||
self.assertGreater(len(tested), 70)
|
||||
|
||||
def test_regression_suites_are_required_in_ci(self):
|
||||
workflow = (ROOT / '.github/workflows/run-unit-tests.yml').read_text(encoding='ascii')
|
||||
self.assertIn('python3 -B test/test_micronmea_time_fix.py -v', workflow)
|
||||
self.assertIn('python3 -B test/test_micronmea_time_fix_recipes.py -v', workflow)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user