mirror of
https://github.com/adafruit/Adafruit_GPS.git
synced 2026-10-26 22:10:17 +00:00
Merge pull request #196 from ladyada-eagleclaw/gps-shared-position-decoder
Decode exact GNSS positions into independent sentence results
This commit is contained in:
4 files changed
+265
-9
No files matched your search
@@ -41,6 +41,11 @@ 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 shared position decoder regression checks GGA/RMC/GLL results, independent
|
||||
fix and GGA-quality status, empty/missing fields, exact coordinates through text
|
||||
output, and rejection of malformed sentences without partial measurements.
|
||||
Returned values remain independent of the input buffer and other receivers.
|
||||
|
||||
Physical reception tests live separately in `extras/hw_tests/gnss_receive`.
|
||||
Build and run them explicitly on the documented Nano or HILBERT fixture; the
|
||||
host test runner does not operate attached boards.
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "Adafruit_GNSS.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static nmea_span_t text(const char *value) {
|
||||
nmea_span_t span = {value, value ? strlen(value) : 0};
|
||||
return span;
|
||||
}
|
||||
|
||||
static gnss_position_t decode(const char *type, const char *fields) {
|
||||
return Adafruit_GNSS::parsePosition(text(type), text(fields));
|
||||
}
|
||||
|
||||
static void rejected(const char *type, const char *fields,
|
||||
gnss_sentence_status_t status, uint8_t field) {
|
||||
gnss_position_t value = decode(type, fields);
|
||||
assert(value.validation.status == status && value.validation.field == field);
|
||||
assert(value.latitude.status == NMEA_NUMBER_MISSING &&
|
||||
value.longitude.status == NMEA_NUMBER_MISSING &&
|
||||
value.time.status == NMEA_NUMBER_MISSING &&
|
||||
value.date.status == NMEA_NUMBER_MISSING &&
|
||||
value.fixStatus == NMEA_NUMBER_MISSING &&
|
||||
value.fixQualityStatus == NMEA_NUMBER_MISSING);
|
||||
assert(!value.latitude.degreesE7 && !value.latitude.degrees &&
|
||||
!value.latitude.minutes && !value.latitude.fractionalMinutes &&
|
||||
!value.latitude.hemisphere && !value.longitude.degreesE7 &&
|
||||
!value.longitude.degrees && !value.longitude.minutes &&
|
||||
!value.longitude.fractionalMinutes && !value.longitude.hemisphere);
|
||||
assert(!value.fix && !value.fixQuality && !value.time.hour &&
|
||||
!value.time.minute && !value.time.second && !value.time.millisecond &&
|
||||
!value.date.day && !value.date.month && !value.date.year);
|
||||
}
|
||||
|
||||
int main() {
|
||||
gnss_position_t rmc = decode(
|
||||
"RMC",
|
||||
"123519.123,A,8959.123456789,S,17959.123456789,W,0.01,0.02,290224");
|
||||
assert(rmc.validation.status == GNSS_SENTENCE_VALID && !rmc.validation.field);
|
||||
assert(rmc.fixStatus == NMEA_NUMBER_VALID && rmc.fix);
|
||||
assert(rmc.fixQualityStatus == NMEA_NUMBER_MISSING && !rmc.fixQuality);
|
||||
assert(rmc.latitude.status == NMEA_NUMBER_VALID &&
|
||||
rmc.longitude.status == NMEA_NUMBER_VALID);
|
||||
assert(rmc.latitude.degrees == 89 && rmc.latitude.minutes == 59 &&
|
||||
rmc.latitude.fractionalMinutes == 123456789 &&
|
||||
rmc.longitude.degrees == 179 && rmc.longitude.minutes == 59 &&
|
||||
rmc.longitude.fractionalMinutes == 123456789);
|
||||
assert(rmc.time.status == NMEA_NUMBER_VALID && rmc.time.hour == 12 &&
|
||||
rmc.time.minute == 35 && rmc.time.second == 19 &&
|
||||
rmc.time.millisecond == 123);
|
||||
assert(rmc.date.status == NMEA_NUMBER_VALID && rmc.date.day == 29 &&
|
||||
rmc.date.month == 2 && rmc.date.year == 24);
|
||||
char output[GNSS_COORDINATE_TEXT_SIZE];
|
||||
assert(Adafruit_GNSS::formatCoordinate(output, sizeof(output), rmc.latitude));
|
||||
assert(!strcmp(output, "-89.98539094648"));
|
||||
assert(
|
||||
Adafruit_GNSS::formatCoordinate(output, sizeof(output), rmc.longitude));
|
||||
assert(!strcmp(output, "-179.98539094648"));
|
||||
puts("PASS: exact RMC coordinates, UTC/date, and GGA-independent fix");
|
||||
|
||||
gnss_position_t gga =
|
||||
decode("GGA", "123520,4807.038,N,01131.000,E,4,08,0.9,545.4,M,-46.9");
|
||||
assert(gga.validation.status == GNSS_SENTENCE_VALID && gga.fix);
|
||||
assert(gga.fixStatus == NMEA_NUMBER_VALID &&
|
||||
gga.fixQualityStatus == NMEA_NUMBER_VALID && gga.fixQuality == 4);
|
||||
assert(gga.latitude.degreesE7 == 481173000 &&
|
||||
gga.longitude.degreesE7 == 115166666 &&
|
||||
gga.date.status == NMEA_NUMBER_MISSING);
|
||||
gnss_position_t gll = decode("GLL", "4807.038,N,01131.000,E,123521,A");
|
||||
assert(gll.validation.status == GNSS_SENTENCE_VALID && gll.fix &&
|
||||
gll.fixStatus == NMEA_NUMBER_VALID && gll.time.second == 21 &&
|
||||
gll.date.status == NMEA_NUMBER_MISSING &&
|
||||
gll.fixQualityStatus == NMEA_NUMBER_MISSING);
|
||||
gll = decode("GLL", "4807.038,N,01131.000,E,123521,V");
|
||||
assert(gll.validation.status == GNSS_SENTENCE_VALID && !gll.fix &&
|
||||
gll.fixStatus == NMEA_NUMBER_VALID &&
|
||||
gll.latitude.status == NMEA_NUMBER_VALID);
|
||||
assert(rmc.latitude.fractionalMinutes == 123456789 && rmc.time.second == 19);
|
||||
puts("PASS: GGA RTK quality, GLL validity, and independent sentence results");
|
||||
|
||||
gnss_position_t empty = decode("RMC", ",V,,,,,,,");
|
||||
assert(empty.validation.status == GNSS_SENTENCE_VALID && !empty.fix);
|
||||
assert(empty.latitude.status == NMEA_NUMBER_EMPTY &&
|
||||
empty.longitude.status == NMEA_NUMBER_EMPTY &&
|
||||
empty.time.status == NMEA_NUMBER_EMPTY &&
|
||||
empty.date.status == NMEA_NUMBER_EMPTY &&
|
||||
empty.fixStatus == NMEA_NUMBER_VALID &&
|
||||
empty.fixQualityStatus == NMEA_NUMBER_MISSING);
|
||||
empty = decode("GGA", ",,,,,0,00,99.99,,M,");
|
||||
assert(empty.validation.status == GNSS_SENTENCE_VALID && !empty.fix &&
|
||||
empty.fixStatus == NMEA_NUMBER_VALID &&
|
||||
empty.fixQualityStatus == NMEA_NUMBER_VALID && !empty.fixQuality &&
|
||||
empty.latitude.status == NMEA_NUMBER_EMPTY);
|
||||
empty = decode("GLL", ",,,,,");
|
||||
assert(empty.validation.status == GNSS_SENTENCE_VALID &&
|
||||
empty.fixStatus == NMEA_NUMBER_EMPTY && !empty.fix);
|
||||
empty = decode("GGA", ",,,,,,,,,,");
|
||||
assert(empty.validation.status == GNSS_SENTENCE_VALID &&
|
||||
empty.fixStatus == NMEA_NUMBER_EMPTY &&
|
||||
empty.fixQualityStatus == NMEA_NUMBER_EMPTY && !empty.fix);
|
||||
puts("PASS: empty fields, missing sentence-specific fields, and no-fix data");
|
||||
|
||||
rejected("RMC", "123519,A,4807.038,N,01131.000,E,1,2,310426",
|
||||
GNSS_SENTENCE_INVALID_FIELD, 9);
|
||||
rejected("GGA", "123519,4807.038,N,01131.000,E,1,08,0.9,NaN,M,1",
|
||||
GNSS_SENTENCE_INVALID_FIELD, 9);
|
||||
rejected("GLL", "4807.038,N,01131.000,N,123519,A",
|
||||
GNSS_SENTENCE_INVALID_FIELD, 3);
|
||||
rejected("GGA", "123519,4807.038", GNSS_SENTENCE_MISSING_FIELDS, 3);
|
||||
rejected("RMC", NULL, GNSS_SENTENCE_MISSING_FIELDS, 1);
|
||||
rejected("GSA", "", GNSS_SENTENCE_UNSUPPORTED, 0);
|
||||
rejected("VTG", NULL, GNSS_SENTENCE_UNSUPPORTED, 0);
|
||||
rejected("GLLX", "", GNSS_SENTENCE_UNSUPPORTED, 0);
|
||||
rejected(NULL, NULL, GNSS_SENTENCE_UNSUPPORTED, 0);
|
||||
puts("PASS: early and late failures return no partial position data");
|
||||
|
||||
char sentence[120];
|
||||
const char *body =
|
||||
"GNRMC,123519,A,8959.123456889,S,17959.123456889,W,1,2,290224";
|
||||
size_t length = Adafruit_NMEA::buildCommand(sentence, sizeof(sentence), body,
|
||||
strlen(body));
|
||||
nmea_sentence_t frame = Adafruit_NMEA::validate(sentence, length);
|
||||
assert(frame.status == NMEA_FRAME_VALID && frame.address.length == 5);
|
||||
gnss_position_t copied =
|
||||
Adafruit_GNSS::parsePosition({frame.address.data + 2, 3}, frame.fields);
|
||||
assert(copied.validation.status == GNSS_SENTENCE_VALID);
|
||||
memset(sentence, 'x', sizeof(sentence));
|
||||
assert(copied.latitude.degreesE7 == rmc.latitude.degreesE7 &&
|
||||
copied.latitude.fractionalMinutes == 123456889);
|
||||
assert(
|
||||
Adafruit_GNSS::formatCoordinate(output, sizeof(output), copied.latitude));
|
||||
assert(!strcmp(output, "-89.98539094815"));
|
||||
const char bounded[] = "4807.038,N,01131.000,E,123519,Aignored";
|
||||
const char kind[] = {'G', 'L', 'L', 'X'};
|
||||
copied =
|
||||
Adafruit_GNSS::parsePosition({kind, 3}, {bounded, sizeof(bounded) - 8});
|
||||
assert(copied.validation.status == GNSS_SENTENCE_VALID && copied.fix);
|
||||
puts("PASS: framed input, owned results, bounded spans, and submillimeter "
|
||||
"text");
|
||||
return 0;
|
||||
}
|
||||
+101
-9
@@ -304,15 +304,7 @@ gnss_validation_t Adafruit_GNSS::validateNavigation(nmea_span_t type,
|
||||
gnss_validation_t result = {GNSS_SENTENCE_UNSUPPORTED, 0};
|
||||
if (!type.data || type.length != 3)
|
||||
return result;
|
||||
uint8_t sentence = UNSUPPORTED;
|
||||
if (type.data[0] == 'G' && type.data[1] == 'G' && type.data[2] == 'A')
|
||||
sentence = GGA;
|
||||
else if (type.data[0] == 'R' && type.data[1] == 'M' && type.data[2] == 'C')
|
||||
sentence = RMC;
|
||||
else if (type.data[0] == 'G' && type.data[1] == 'L' && type.data[2] == 'L')
|
||||
sentence = GLL;
|
||||
else if (type.data[0] == 'G' && type.data[1] == 'S' && type.data[2] == 'A')
|
||||
sentence = GSA;
|
||||
uint8_t sentence = sentenceType(type);
|
||||
if (sentence == UNSUPPORTED)
|
||||
return result;
|
||||
uint8_t required = 6, latitudeField = 1, timeField = 5;
|
||||
@@ -387,6 +379,106 @@ gnss_validation_t Adafruit_GNSS::validateNavigation(nmea_span_t type,
|
||||
return result;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Identify a supported three-character standard sentence type.
|
||||
@param type Three-character identifier already bounded by the caller.
|
||||
@return Private sentence kind, or UNSUPPORTED.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
uint8_t Adafruit_GNSS::sentenceType(nmea_span_t type) {
|
||||
uint8_t sentence = UNSUPPORTED;
|
||||
if (type.data[0] == 'G' && type.data[1] == 'G' && type.data[2] == 'A')
|
||||
sentence = GGA;
|
||||
else if (type.data[0] == 'R' && type.data[1] == 'M' && type.data[2] == 'C')
|
||||
sentence = RMC;
|
||||
else if (type.data[0] == 'G' && type.data[1] == 'L' && type.data[2] == 'L')
|
||||
sentence = GLL;
|
||||
else if (type.data[0] == 'G' && type.data[1] == 'S' && type.data[2] == 'A')
|
||||
sentence = GSA;
|
||||
return sentence;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Decode a GGA, RMC, or GLL position without retaining receiver state.
|
||||
@param type Three-character sentence type without its talker prefix.
|
||||
@param fields Borrowed fields after the address comma and before '*'.
|
||||
@return Independent values and validation diagnostics. Invalid or
|
||||
unsupported input returns no partially decoded measurements.
|
||||
|
||||
Call after validating the enclosing frame with Adafruit_NMEA::validate().
|
||||
Input storage must remain readable and unchanged throughout the call.
|
||||
All navigation fields consumed by the GPS decoder are validated first,
|
||||
including fields this position result does not expose (such as altitude).
|
||||
Optional tails retain validateNavigation()'s existing behavior.
|
||||
|
||||
Populated coordinates retain all nine fractional-minute digits and can be
|
||||
passed directly to formatCoordinate(). Empty fields have EMPTY status;
|
||||
fields absent from this sentence type have MISSING status. The fix boolean
|
||||
is meaningful only with VALID fixStatus and does not imply populated
|
||||
coordinates. GGA quality is independent of RMC/GLL fix validity.
|
||||
|
||||
Time/date and exact position components belong only to this sentence. No
|
||||
timestamps, previous-fix merging, allocation, or floating-point conversion
|
||||
occur here. GSA and other sentence types are UNSUPPORTED by this decoder.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
gnss_position_t Adafruit_GNSS::parsePosition(nmea_span_t type,
|
||||
nmea_span_t fields) {
|
||||
gnss_position_t result = {};
|
||||
result.validation.status = GNSS_SENTENCE_UNSUPPORTED;
|
||||
result.latitude.status = result.longitude.status = NMEA_NUMBER_MISSING;
|
||||
result.time.status = result.date.status = NMEA_NUMBER_MISSING;
|
||||
result.fixStatus = result.fixQualityStatus = NMEA_NUMBER_MISSING;
|
||||
if (!type.data || type.length != 3)
|
||||
return result;
|
||||
uint8_t sentence = sentenceType(type);
|
||||
if (sentence != GGA && sentence != RMC && sentence != GLL)
|
||||
return result;
|
||||
result.validation = validateNavigation(type, fields);
|
||||
if (result.validation.status != GNSS_SENTENCE_VALID)
|
||||
return result;
|
||||
|
||||
uint8_t latitudeField = 1, timeField = 5, fixField = 6, lastField = 6;
|
||||
if (sentence == GGA) {
|
||||
latitudeField = 2;
|
||||
timeField = 1;
|
||||
} else if (sentence == RMC) {
|
||||
latitudeField = 3;
|
||||
timeField = 1;
|
||||
fixField = 2;
|
||||
lastField = 9;
|
||||
}
|
||||
for (uint8_t i = 1; i <= lastField; i++) {
|
||||
nmea_span_t field = Adafruit_NMEA::nextField(fields);
|
||||
if (i == latitudeField || i == latitudeField + 2) {
|
||||
nmea_span_t hemisphere = Adafruit_NMEA::nextField(fields);
|
||||
if (i == latitudeField)
|
||||
result.latitude = parseCoordinate(field, hemisphere);
|
||||
else
|
||||
result.longitude = parseCoordinate(field, hemisphere);
|
||||
i++;
|
||||
} else if (i == timeField) {
|
||||
result.time = parseTime(field);
|
||||
} else if (i == fixField) {
|
||||
result.fixStatus = field.length ? NMEA_NUMBER_VALID : NMEA_NUMBER_EMPTY;
|
||||
if (sentence == GGA) {
|
||||
result.fixQualityStatus = result.fixStatus;
|
||||
// Validation already proved that this integer fits uint8_t.
|
||||
for (size_t j = 0; j < field.length; j++)
|
||||
result.fixQuality = result.fixQuality * 10 + field.data[j] - '0';
|
||||
result.fix = result.fixQuality > 0;
|
||||
} else {
|
||||
result.fix = field.length && field.data[0] == 'A';
|
||||
}
|
||||
} else if (sentence == RMC && i == 9) {
|
||||
result.date = parseDate(field);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Keep integer-only byte fields exact; accepting atof-style prefixes can wrap.
|
||||
static bool validUnsignedByte(nmea_span_t field) {
|
||||
uint16_t value = 0;
|
||||
|
||||
@@ -59,6 +59,22 @@ typedef struct {
|
||||
uint8_t field; ///< One-based error field, or zero for valid/unsupported.
|
||||
} gnss_validation_t;
|
||||
|
||||
/** One sentence's position data, independent of earlier receiver state.
|
||||
* Check validation first, then each field's status. A valid sentence does not
|
||||
* imply a position fix or populated coordinates. Failed/unsupported decoding
|
||||
* leaves all measurement values zero and all field statuses MISSING. */
|
||||
typedef struct {
|
||||
gnss_validation_t validation; ///< Sentence status and first failing field.
|
||||
gnss_coordinate_t latitude; ///< Exact latitude components.
|
||||
gnss_coordinate_t longitude; ///< Exact longitude components.
|
||||
gnss_time_t time; ///< UTC time, or EMPTY for a blank time field.
|
||||
gnss_date_t date; ///< RMC date; MISSING in GGA/GLL, possibly EMPTY.
|
||||
nmea_number_status_t fixStatus; ///< Fix field status.
|
||||
bool fix; ///< Valid fix; check fixStatus first.
|
||||
nmea_number_status_t fixQualityStatus; ///< Quality field status.
|
||||
uint8_t fixQuality; ///< GGA quality, including RTK values; check its status.
|
||||
} gnss_position_t;
|
||||
|
||||
/** Stateless standard GNSS decoding shared by receiver implementations.
|
||||
* Inputs are borrowed spans. Decoding neither allocates memory nor performs
|
||||
* transport I/O, and does not alter the supplied text or receiver state.
|
||||
@@ -69,12 +85,14 @@ public:
|
||||
nmea_span_t hemisphere);
|
||||
static size_t formatCoordinate(char *output, size_t capacity,
|
||||
const gnss_coordinate_t &coordinate);
|
||||
static gnss_position_t parsePosition(nmea_span_t type, nmea_span_t fields);
|
||||
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,
|
||||
nmea_span_t fields);
|
||||
|
||||
private:
|
||||
static uint8_t sentenceType(nmea_span_t type);
|
||||
/// Sentence kinds handled by the navigation validator.
|
||||
enum {
|
||||
UNSUPPORTED, ///< Sentence has no standard navigation validation.
|
||||
|
||||
Reference in new issue
Block a user