mirror of
https://github.com/adafruit/Adafruit_GPS.git
synced 2026-10-27 06:20:18 +00:00
Preserve GNSS coordinate precision when formatting decimal degrees
Format retained coordinate components with integer long division so AVR logging can retain submillimeter detail without a floating-point type override. Add bounded-output and precision regressions. Co-authored-by: Limor Fried <ladyada@users.noreply.github.com>
This commit is contained in:
1 parent
6cbdadb512
commit
46f0ff40a1
4 files changed
+227
-4
No files matched your search
@@ -32,10 +32,14 @@ The sketches remain usable on Arduino boards with the normal Arduino core.
|
||||
Coordinate precision tests preserve all nine retained fractional-minute digits
|
||||
in the GNSS core and distinguish positions about 0.185 mm apart through the GPS
|
||||
double output, including southern latitude and longitude near 180 degrees west.
|
||||
Those positions deliberately share the same legacy E7 coordinate. The default
|
||||
float fields and E7 fields are not full-precision RTK outputs. Future receiver
|
||||
APIs and logging examples must preserve the core precision through storage,
|
||||
public results, and formatting without requiring a global float-type override.
|
||||
The coordinate-text regression also checks integer-only decimal-degree output
|
||||
with 11 fractional digits, including adjacent retained fractional-minute values
|
||||
and every output capacity. This output preserves detail on AVR without a global
|
||||
float-type override. The submillimeter positions deliberately share the same
|
||||
legacy E7 coordinate. The default float fields and E7 fields are not
|
||||
full-precision RTK outputs. Future receiver APIs and logging examples must
|
||||
preserve the core precision through storage, public results, and formatting
|
||||
without requiring a global float-type override.
|
||||
|
||||
Physical reception tests live separately in `extras/hw_tests/gnss_receive`.
|
||||
Build and run them explicitly on the documented Nano or HILBERT fixture; the
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#include <Adafruit_GNSS.h>
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
bool testsPassed = false;
|
||||
bool checkText(const gnss_coordinate_t &coordinate, const char *expected);
|
||||
bool checkRejected(const gnss_coordinate_t &coordinate);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println(F("Adafruit GNSS exact coordinate text regression"));
|
||||
const struct {
|
||||
const char *field;
|
||||
const char *hemisphere;
|
||||
const char *expected;
|
||||
} cases[] = {
|
||||
{"4807.038123456", "N", "48.11730205760"},
|
||||
{"4807.038123456", "S", "-48.11730205760"},
|
||||
{"01131.000", "E", "11.51666666666"},
|
||||
{"17959.123456789", "W", "-179.98539094648"},
|
||||
{"8959.123456789", "S", "-89.98539094648"},
|
||||
{"8959.123456889", "S", "-89.98539094815"},
|
||||
{"8959.999999999", "N", "89.99999999998"},
|
||||
{"0000.000000001", "N", "0.00000000001"},
|
||||
{"0000.000000002", "N", "0.00000000003"},
|
||||
{"0000", "N", "0.00000000000"},
|
||||
{"0000", "S", "-0.00000000000"},
|
||||
{"00000", "W", "-0.00000000000"},
|
||||
{"9000", "S", "-90.00000000000"},
|
||||
{"18000", "E", "180.00000000000"},
|
||||
{"18000", "W", "-180.00000000000"}
|
||||
};
|
||||
for (uint8_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
gnss_coordinate_t coordinate = Adafruit_GNSS::parseCoordinate(
|
||||
{cases[i].field, strlen(cases[i].field)}, {cases[i].hemisphere, 1});
|
||||
if (coordinate.status != NMEA_NUMBER_VALID ||
|
||||
!checkText(coordinate, cases[i].expected)) {
|
||||
Serial.println(F("FAIL: exact text, truncation, or output buffer bounds"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Serial.println(F("PASS: hemispheres, boundaries, and all output capacities"));
|
||||
Serial.println(F("PASS: submillimeter and adjacent fractional-minute values"));
|
||||
|
||||
gnss_coordinate_t valid = Adafruit_GNSS::parseCoordinate({"4807.038", 8}, {"N", 1});
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
gnss_coordinate_t invalid = valid;
|
||||
if (i == 0) {
|
||||
invalid.status = NMEA_NUMBER_EMPTY;
|
||||
}
|
||||
if (i == 1) {
|
||||
invalid.hemisphere = 'X';
|
||||
}
|
||||
if (i == 2) {
|
||||
invalid.minutes = 60;
|
||||
}
|
||||
if (i == 3) {
|
||||
invalid.fractionalMinutes = 1000000000UL;
|
||||
}
|
||||
if (i == 4) {
|
||||
invalid.degrees = 91;
|
||||
}
|
||||
if (i == 5) {
|
||||
invalid.degrees = 90; // Nonzero minutes at the pole.
|
||||
}
|
||||
if (i == 6) {
|
||||
invalid.hemisphere = 'W';
|
||||
invalid.degrees = 180; // Nonzero minutes at the date line.
|
||||
}
|
||||
if (!checkRejected(invalid)) {
|
||||
Serial.println(F("FAIL: invalid coordinate components were formatted"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
gnss_coordinate_t empty = {};
|
||||
if (!checkRejected(empty) ||
|
||||
Adafruit_GNSS::formatCoordinate(NULL, GNSS_COORDINATE_TEXT_SIZE, valid)) {
|
||||
Serial.println(F("FAIL: empty coordinate or NULL output accepted"));
|
||||
return;
|
||||
}
|
||||
// The E7 convenience field must never be the source of precise text.
|
||||
valid.degreesE7 = 0;
|
||||
if (!checkText(valid, "48.11730000000")) {
|
||||
Serial.println(F("FAIL: output depended on the lower-resolution E7 value"));
|
||||
return;
|
||||
}
|
||||
Serial.println(F("PASS: invalid components rejected; E7 is not used"));
|
||||
testsPassed = true;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (testsPassed) {
|
||||
Serial.println(F("PASS: exact coordinate text regression"));
|
||||
} else {
|
||||
Serial.println(F("FAIL: exact coordinate text regression"));
|
||||
}
|
||||
}
|
||||
|
||||
bool checkText(const gnss_coordinate_t &coordinate, const char *expected) {
|
||||
size_t length = strlen(expected);
|
||||
for (size_t capacity = 0; capacity <= GNSS_COORDINATE_TEXT_SIZE; capacity++) {
|
||||
char storage[GNSS_COORDINATE_TEXT_SIZE + 2];
|
||||
memset(storage, '?', sizeof(storage));
|
||||
char *output = storage + 1;
|
||||
size_t written = Adafruit_GNSS::formatCoordinate(output, capacity, coordinate);
|
||||
size_t changed = capacity ? 1 : 0;
|
||||
if (capacity > length) {
|
||||
if (written != length || strcmp(output, expected)) {
|
||||
return false;
|
||||
}
|
||||
changed = length + 1;
|
||||
} else if (written || (capacity && output[0])) {
|
||||
return false;
|
||||
}
|
||||
if (storage[0] != '?') {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = changed + 1; i < sizeof(storage); i++) {
|
||||
if (storage[i] != '?') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkRejected(const gnss_coordinate_t &coordinate) {
|
||||
char output[GNSS_COORDINATE_TEXT_SIZE];
|
||||
memset(output, '?', sizeof(output));
|
||||
if (Adafruit_GNSS::formatCoordinate(output, sizeof(output), coordinate) || output[0]) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 1; i < sizeof(output); i++) {
|
||||
if (output[i] != '?') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -100,6 +100,81 @@ gnss_coordinate_t Adafruit_GNSS::parseCoordinate(nmea_span_t coordinate,
|
||||
return result;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Format exact coordinate components as signed decimal degrees.
|
||||
@param output Writable text buffer; must not overlap coordinate.
|
||||
@param capacity Buffer size including the NUL terminator. Use
|
||||
GNSS_COORDINATE_TEXT_SIZE to fit every valid coordinate.
|
||||
@param coordinate Validated components from parseCoordinate().
|
||||
@return Characters written, excluding NUL, or zero for invalid components
|
||||
or insufficient storage. On failure output[0] is cleared when possible;
|
||||
no other output bytes are changed.
|
||||
|
||||
Writes exactly 11 fractional degree digits, truncated toward zero. This
|
||||
distinguishes every retained fractional-minute increment and introduces
|
||||
less than 0.00000000001 degree of formatting error (about 1.2 micrometers
|
||||
of latitude). Southern/western zero retains its minus sign. The components,
|
||||
not the lower-resolution degreesE7 member, supply the result.
|
||||
|
||||
Integer long division uses at most 32-bit arithmetic, with no heap or
|
||||
floating-point conversion. AVR output retains the same precision as other
|
||||
targets, regardless of NMEA_FLOAT_T or the platform's double size.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
size_t Adafruit_GNSS::formatCoordinate(char *output, size_t capacity,
|
||||
const gnss_coordinate_t &coordinate) {
|
||||
if (!output || !capacity)
|
||||
return 0;
|
||||
output[0] = '\0';
|
||||
char direction = coordinate.hemisphere;
|
||||
bool latitude = direction == 'N' || direction == 'S';
|
||||
uint16_t limit = latitude ? 90 : 180;
|
||||
uint16_t degrees = coordinate.degrees;
|
||||
uint16_t remainder = coordinate.minutes;
|
||||
uint32_t fraction = coordinate.fractionalMinutes;
|
||||
if (coordinate.status != NMEA_NUMBER_VALID ||
|
||||
(!latitude && direction != 'E' && direction != 'W') || degrees > limit ||
|
||||
remainder >= 60 || fraction >= 1000000000UL ||
|
||||
(degrees == limit && (remainder || fraction)))
|
||||
return 0;
|
||||
bool negative = direction == 'S' || direction == 'W';
|
||||
uint8_t digits = 1;
|
||||
uint16_t divisor = 1;
|
||||
if (degrees >= 100) {
|
||||
digits = 3;
|
||||
divisor = 100;
|
||||
} else if (degrees >= 10) {
|
||||
digits = 2;
|
||||
divisor = 10;
|
||||
}
|
||||
// Account for the sign, decimal point, eleven fractional digits, and NUL.
|
||||
if (capacity < (size_t)(negative + digits + 13))
|
||||
return 0;
|
||||
size_t written = 0;
|
||||
if (negative)
|
||||
output[written++] = '-';
|
||||
while (divisor) {
|
||||
output[written++] = '0' + degrees / divisor;
|
||||
degrees %= divisor;
|
||||
divisor /= 10;
|
||||
}
|
||||
output[written++] = '.';
|
||||
uint32_t place = 100000000UL;
|
||||
for (uint8_t i = 0; i < 11; i++) {
|
||||
remainder *= 10;
|
||||
if (place) {
|
||||
remainder += fraction / place;
|
||||
fraction %= place;
|
||||
place /= 10;
|
||||
}
|
||||
output[written++] = '0' + remainder / 60;
|
||||
remainder %= 60;
|
||||
}
|
||||
output[written] = '\0';
|
||||
return written;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Decode a complete UTC hhmmss[.fraction] field.
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
#include "Adafruit_NMEA.h"
|
||||
|
||||
/// Maximum coordinate text size: sign, three degrees, dot, 11 decimals, NUL.
|
||||
#define GNSS_COORDINATE_TEXT_SIZE 17
|
||||
|
||||
/** Validated coordinate components, with no floating-point conversion.
|
||||
* Every value except status is zero on failure. Southern and western
|
||||
* coordinates have negative degreesE7; the other components are unsigned.
|
||||
@@ -64,6 +67,8 @@ class Adafruit_GNSS {
|
||||
public:
|
||||
static gnss_coordinate_t parseCoordinate(nmea_span_t coordinate,
|
||||
nmea_span_t hemisphere);
|
||||
static size_t formatCoordinate(char *output, size_t capacity,
|
||||
const gnss_coordinate_t &coordinate);
|
||||
static gnss_time_t parseTime(nmea_span_t field);
|
||||
static gnss_date_t parseDate(nmea_span_t field);
|
||||
static gnss_validation_t validateNavigation(nmea_span_t type,
|
||||
|
||||
Reference in new issue
Block a user