Merge pull request #193 from ladyada-eagleclaw/gps-navigation-validation

Validate navigation fields before updating GPS data
This commit is contained in:
Limor "Ladyada" Fried authored and GitHub committed 2026-09-15 22:10:59 -04:00
commit 092b468d37
11 files changed
+746 -126

No files matched your search

+15 -9
View File
@@ -37,7 +37,9 @@ bool usingInterrupt = false;
#define ledPin 13
SdFat SD;
File logfile;
// Raw NMEA logging only needs the base file API. Avoid the Arduino Stream
// wrapper, whose virtual methods also pull unused read/seek code into flash.
SdBaseFile logfile;
// read a Hex value and return the decimal equivalent
uint8_t parseHex(char c) {
@@ -98,15 +100,19 @@ void setup() {
}
char filename[15];
strcpy(filename, "GPSLOG0000.TXT");
for (uint16_t i = 0; i < 10000; i++) {
filename[6] = '0' + i/1000;
filename[7] = '0' + (i/100)%10;
filename[8] = '0' + (i/10)%10;
filename[9] = '0' + i%10;
// Find an unused long filename, then create it without opening an old log.
if (! SD.exists(filename)) {
// Find an unused long filename, then create it without opening an old log.
while (SD.exists(filename)) {
// Carry through the four digits, keeping the filename as the counter.
int8_t digit = 9;
while (digit >= 6 && filename[digit] == '9') {
filename[digit] = '0';
digit--;
}
if (digit < 6) {
// All 10000 names exist. Exclusive creation below will fail safely.
break;
}
filename[digit]++;
}
if (!logfile.open(filename, O_WRONLY | O_CREAT | O_EXCL)) {
@@ -219,7 +225,7 @@ void loop() {
uint8_t stringsize = strlen(stringptr);
if (stringsize != logfile.write((uint8_t *)stringptr, stringsize)) //write the string to the SD file
error(4);
if (strstr(stringptr, "RMC") || strstr(stringptr, "GGA")) logfile.flush();
if (strstr(stringptr, "RMC") || strstr(stringptr, "GGA")) logfile.sync();
Serial.println();
}
}
@@ -0,0 +1,100 @@
#include "Adafruit_GNSS.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <string>
static nmea_span_t text(const char *value) {
nmea_span_t span = {value, value ? strlen(value) : 0};
return span;
}
static void expect(const char *type, const char *fields,
gnss_sentence_status_t status, uint8_t field = 0) {
gnss_validation_t result =
Adafruit_GNSS::validateNavigation(text(type), text(fields));
if (result.status != status || result.field != field) {
fprintf(stderr, "%s expected %u/%u, got %u/%u: %s\n", type, status, field,
result.status, result.field, fields ? fields : "NULL");
assert(false);
}
}
static std::string replace(const char *fields, uint8_t index,
const char *value) {
std::string result;
nmea_span_t remaining = text(fields);
for (uint8_t i = 1; remaining.data; i++) {
nmea_span_t field = Adafruit_NMEA::nextField(remaining);
if (i > 1)
result += ',';
if (i == index)
result += value;
else
result.append(field.data, field.length);
}
return result;
}
int main() {
const char *gga = "123519.123,4807.038,N,01131.000,E,1,08,0.9,545.4,M,-46.9";
const char *rmc = "123519,A,4807.038,N,01131.000,E,22.4,84.4,150926";
const char *gll = "4807.038,N,01131.000,E,123519,A";
const char *gsa = "A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.5,0.9,1.2";
expect("GGA", gga, GNSS_SENTENCE_VALID);
expect("RMC", rmc, GNSS_SENTENCE_VALID);
expect("GLL", gll, GNSS_SENTENCE_VALID);
expect("GSA", gsa, GNSS_SENTENCE_VALID);
expect("RMC", ",V,,,,,,,", GNSS_SENTENCE_VALID);
expect("GGA", ",,,,,0,00,99.99,,M,", GNSS_SENTENCE_VALID);
expect("GLL", ",,,,,V", GNSS_SENTENCE_VALID);
expect("VTG", "anything", GNSS_SENTENCE_UNSUPPORTED);
expect("GGAX", gga, GNSS_SENTENCE_UNSUPPORTED);
expect(NULL, NULL, GNSS_SENTENCE_UNSUPPORTED);
puts("PASS: navigation types, empty no-fix fields, and unsupported types");
struct Invalid {
uint8_t index;
const char *value;
};
const Invalid badGga[] = {
{1, "246000"}, {2, "4860.0"}, {2, "4807x"}, {3, "E"}, {3, ""},
{4, "01131x"}, {5, "N"}, {6, "256"}, {6, "1.0"}, {7, "99999"},
{8, "-0.1"}, {8, "0.9x"}, {9, "NaN"}, {10, "F"}, {11, "12e2"}};
for (size_t i = 0; i < sizeof(badGga) / sizeof(badGga[0]); i++) {
uint8_t field = badGga[i].index;
if (field == 3 || field == 5)
field--;
expect("GGA", replace(gga, badGga[i].index, badGga[i].value).c_str(),
GNSS_SENTENCE_INVALID_FIELD, field);
}
expect("RMC", replace(rmc, 2, "Active").c_str(), GNSS_SENTENCE_INVALID_FIELD,
2);
expect("RMC", replace(rmc, 7, "-1").c_str(), GNSS_SENTENCE_INVALID_FIELD, 7);
expect("RMC", replace(rmc, 8, "1.2x").c_str(), GNSS_SENTENCE_INVALID_FIELD,
8);
expect("RMC", replace(rmc, 9, "310426").c_str(), GNSS_SENTENCE_INVALID_FIELD,
9);
expect("GLL", replace(gll, 5, "120000.1x").c_str(),
GNSS_SENTENCE_INVALID_FIELD, 5);
expect("GLL", replace(gll, 6, "Q").c_str(), GNSS_SENTENCE_INVALID_FIELD, 6);
expect("GSA", replace(gsa, 2, "256").c_str(), GNSS_SENTENCE_INVALID_FIELD, 2);
expect("GSA", replace(gsa, 15, "NaN").c_str(), GNSS_SENTENCE_INVALID_FIELD,
15);
expect("GSA", replace(gsa, 16, "-1").c_str(), GNSS_SENTENCE_INVALID_FIELD,
16);
expect("GSA", replace(gsa, 17, "1.2x").c_str(), GNSS_SENTENCE_INVALID_FIELD,
17);
puts("PASS: field identity, syntax, range, and complete coordinate pairs");
expect("GGA", NULL, GNSS_SENTENCE_MISSING_FIELDS, 1);
expect("GGA", "123519,4807.038", GNSS_SENTENCE_MISSING_FIELDS, 3);
expect("RMC", "123519,A,4807.038,N,01131.000,E,22.4,84.4",
GNSS_SENTENCE_MISSING_FIELDS, 9);
expect("GLL", "4807.038,N,01131.000,E,123519", GNSS_SENTENCE_MISSING_FIELDS,
6);
// Optional fields beyond the supported decoder's last field remain untouched.
expect("RMC", (std::string(rmc) + ",,,A,V").c_str(), GNSS_SENTENCE_VALID);
puts("PASS: missing field positions and supported optional tails");
return 0;
}
@@ -0,0 +1,85 @@
#include "Adafruit_GNSS.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static nmea_span_t field(const char *text) {
nmea_span_t result = {text, text ? strlen(text) : 0};
return result;
}
static void badTime(const char *text, nmea_number_status_t status) {
gnss_time_t value = Adafruit_GNSS::parseTime(field(text));
assert(value.status == status);
assert(value.hour == 0 && value.minute == 0 && value.second == 0 &&
value.millisecond == 0);
}
static void badDate(const char *text, nmea_number_status_t status) {
gnss_date_t value = Adafruit_GNSS::parseDate(field(text));
assert(value.status == status);
assert(value.day == 0 && value.month == 0 && value.year == 0);
}
int main() {
gnss_time_t time = Adafruit_GNSS::parseTime(field("123456.789123456789"));
assert(time.status == NMEA_NUMBER_VALID && time.hour == 12 &&
time.minute == 34 && time.second == 56 && time.millisecond == 789);
time = Adafruit_GNSS::parseTime(field("000000"));
assert(time.status == NMEA_NUMBER_VALID && time.hour == 0 &&
time.minute == 0 && time.second == 0 && time.millisecond == 0);
time = Adafruit_GNSS::parseTime(field("235960.1"));
assert(time.status == NMEA_NUMBER_VALID && time.hour == 23 &&
time.minute == 59 && time.second == 60 && time.millisecond == 100);
assert(Adafruit_GNSS::parseTime(field("010203.01")).millisecond == 10);
assert(Adafruit_GNSS::parseTime(field("010203.001")).millisecond == 1);
assert(Adafruit_GNSS::parseTime(field("010203.0009999")).millisecond == 0);
assert(Adafruit_GNSS::parseTime(field("010203.9999999")).millisecond == 999);
puts("PASS: time components, fractional truncation, and leap-second range");
badTime(NULL, NMEA_NUMBER_MISSING);
badTime("", NMEA_NUMBER_EMPTY);
const char *badTimes[] = {"12345", "1234567",
"123456.", "123456.0x",
"+123456", "123456 ",
"123456.1.2", "123456e2",
"12a456", "123456.0000000000000000x"};
for (size_t i = 0; i < sizeof(badTimes) / sizeof(badTimes[0]); i++)
badTime(badTimes[i], NMEA_NUMBER_BAD_FORMAT);
badTime("240000", NMEA_NUMBER_OUT_OF_RANGE);
badTime("126000", NMEA_NUMBER_OUT_OF_RANGE);
badTime("125961", NMEA_NUMBER_OUT_OF_RANGE);
badTime("999999", NMEA_NUMBER_OUT_OF_RANGE);
puts("PASS: invalid times clear every component");
gnss_date_t date = Adafruit_GNSS::parseDate(field("150926"));
assert(date.status == NMEA_NUMBER_VALID && date.day == 15 &&
date.month == 9 && date.year == 26);
assert(Adafruit_GNSS::parseDate(field("290224")).status == NMEA_NUMBER_VALID);
assert(Adafruit_GNSS::parseDate(field("290200")).status == NMEA_NUMBER_VALID);
assert(Adafruit_GNSS::parseDate(field("311299")).year == 99);
assert(Adafruit_GNSS::parseDate(field("010180")).year == 80);
badDate(NULL, NMEA_NUMBER_MISSING);
badDate("", NMEA_NUMBER_EMPTY);
badDate("15092", NMEA_NUMBER_BAD_FORMAT);
badDate("1509260", NMEA_NUMBER_BAD_FORMAT);
badDate("150926.0", NMEA_NUMBER_BAD_FORMAT);
badDate("15a926", NMEA_NUMBER_BAD_FORMAT);
badDate("000926", NMEA_NUMBER_OUT_OF_RANGE);
badDate("150026", NMEA_NUMBER_OUT_OF_RANGE);
badDate("151326", NMEA_NUMBER_OUT_OF_RANGE);
badDate("310426", NMEA_NUMBER_OUT_OF_RANGE);
badDate("290226", NMEA_NUMBER_OUT_OF_RANGE);
badDate("300224", NMEA_NUMBER_OUT_OF_RANGE);
puts("PASS: dates, month lengths, leap years, and two-digit-year "
"preservation");
const char boundedTime[] = {'1', '2', '3', '4', '5', '6', '.', '7', 'x'};
time = Adafruit_GNSS::parseTime({boundedTime, 8});
assert(time.status == NMEA_NUMBER_VALID && time.millisecond == 700);
const char boundedDate[] = {'2', '9', '0', '2', '2', '4', 'x'};
date = Adafruit_GNSS::parseDate({boundedDate, 6});
assert(date.status == NMEA_NUMBER_VALID && date.day == 29);
puts("PASS: bounded fields do not require NUL terminators");
return 0;
}
@@ -0,0 +1,120 @@
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#define private public
#include <Adafruit_GPS.h>
#undef private
Adafruit_GPS GPS;
bool testsPassed = false;
struct NavigationState {
uint8_t hour, minute, second, day, month, year, quality, quality3d, satellites;
uint16_t milliseconds;
int32_t latitude, longitude;
nmea_float_t altitude, geoid, speed, angle, hdop, pdop, vdop;
uint32_t updateTime, fixTime, timeTime, dateTime;
bool fix;
char source[NMEA_MAX_SOURCE_ID], sentence[NMEA_MAX_SENTENCE_ID];
};
bool parseBody(const char *body);
NavigationState saveState();
bool sameState(const NavigationState &before);
void setup() {
Serial.begin(115200);
Serial.println(F("Adafruit GPS coherent navigation regression"));
GPS.resetSentTime();
if (!parseBody("$GPRMC,123519.123,A,4807.038,N,01131.000,E,22.4,84.4,150926,,,A") ||
!parseBody("$GPGGA,123519.123,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9") ||
!parseBody("$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.5,0.9,1.2")) {
Serial.println(F("FAIL: valid navigation seed"));
return;
}
NavigationState before = saveState();
const char *bad[] = {
"$GPRMC,101112,Q,5000.0,N,02000.0,E,1,2,160926",
"$GPRMC,101112,A,5060.0,N,02000.0,E,1,2,160926",
"$GPRMC,101112,A,5000.0,N,02000.0,E,1x,2,160926",
"$GPRMC,101112,A,5000.0,N,02000.0,E,1,2,310426",
"$GPGGA,101112,5000.0,N,02000.0,E,256,09,1,100,M,40",
"$GPGGA,101112,5000.0,N,02000.0,E,2,09,-1,100,M,40",
"$GPGGA,101112,5000.0,N,02000.0,E,2,09,1,100,M,40x",
"$GPGGA,101112x,5000.0,N,02000.0,E,2,09,1,100,M,40",
"$GPGLL,5000.0,N,02000.0,E,101112,Q",
"$GPGLL,05000.0,E,02000.0,E,101112,A",
"$GPGSA,A,2,01,02,03,04,05,06,07,08,09,10,11,12,2.5,1.9,2x"};
for (uint8_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
delay(2);
GPS.resetSentTime();
if (parseBody(bad[i]) || !sameState(before)) {
Serial.print(F("FAIL: rejected sentence changed navigation, case "));
Serial.println(i);
return;
}
}
Serial.println(F("PASS: bad early and late fields preserve navigation and timestamps"));
if (!parseBody("$GPRMC,235960.123999,A,5000.0,S,02000.0,W,1,2,290224") ||
GPS.hour != 23 || GPS.minute != 59 || GPS.seconds != 60 ||
GPS.milliseconds != 123 || GPS.day != 29 || GPS.month != 2 || GPS.year != 24) {
Serial.println(F("FAIL: leap time, millisecond truncation, or date decoding"));
return;
}
Serial.println(F("PASS: shared UTC and date decoding through the RMC parser"));
int32_t latitude = GPS.latitude_fixed, longitude = GPS.longitude_fixed;
uint32_t timeTime = GPS.lastTime;
if (!parseBody("$GPGLL,,,,,,V") || GPS.fix ||
GPS.latitude_fixed != latitude || GPS.longitude_fixed != longitude ||
GPS.lastTime != timeTime) {
Serial.println(F("FAIL: empty no-fix fields replaced stored values or timestamps"));
return;
}
Serial.println(F("PASS: valid no-fix messages preserve absent measurements"));
testsPassed = true;
}
void loop() {
if (testsPassed) {
Serial.println(F("PASS: coherent navigation regression"));
} else {
Serial.println(F("FAIL: coherent navigation regression"));
}
}
bool parseBody(const char *body) {
char sentence[150];
strcpy(sentence, body);
GPS.addChecksum(sentence);
return GPS.parse(sentence);
}
NavigationState saveState() {
NavigationState state = {
GPS.hour, GPS.minute, GPS.seconds, GPS.day, GPS.month, GPS.year,
GPS.fixquality, GPS.fixquality_3d, GPS.satellites, GPS.milliseconds,
GPS.latitude_fixed, GPS.longitude_fixed, GPS.altitude, GPS.geoidheight,
GPS.speed, GPS.angle, GPS.HDOP, GPS.PDOP, GPS.VDOP,
GPS.lastUpdate, GPS.lastFix, GPS.lastTime, GPS.lastDate, GPS.fix, {}, {}};
strcpy(state.source, GPS.lastSource);
strcpy(state.sentence, GPS.lastSentence);
return state;
}
bool sameState(const NavigationState &before) {
NavigationState after = saveState();
return before.hour == after.hour && before.minute == after.minute &&
before.second == after.second && before.milliseconds == after.milliseconds &&
before.day == after.day && before.month == after.month && before.year == after.year &&
before.quality == after.quality && before.quality3d == after.quality3d &&
before.satellites == after.satellites && before.latitude == after.latitude &&
before.longitude == after.longitude && before.altitude == after.altitude &&
before.geoid == after.geoid && before.speed == after.speed && before.angle == after.angle &&
before.hdop == after.hdop && before.pdop == after.pdop && before.vdop == after.vdop &&
before.updateTime == after.updateTime && before.fixTime == after.fixTime &&
before.timeTime == after.timeTime && before.dateTime == after.dateTime &&
before.fix == after.fix && !strcmp(before.source, after.source) &&
!strcmp(before.sentence, after.sentence);
}
+10 -1
View File
@@ -28,6 +28,9 @@ int main() {
{"-.5", -5, 1},
{"1.", 1, 0},
{"00012.0300", 120300, 4},
{"0009223372036854775807", INT64_MAX, 0},
{"-0009223372036854775808", INT64_MIN, 0},
{"1000000000000000000", INT64_C(1000000000000000000), 0},
{"4807.038123456", INT64_C(4807038123456), 9},
{"9223372036854775807", INT64_MAX, 0},
{"-9223372036854775808", INT64_MIN, 0},
@@ -44,7 +47,8 @@ int main() {
for (const char *text : badNumbers)
expectNumber({text, strlen(text)}, NMEA_NUMBER_BAD_FORMAT);
const char *overflow[] = {"9223372036854775808", "-9223372036854775809",
"99999999999999999999999999"};
"99999999999999999999999999",
"10000000000000000000", "0009223372036854775808"};
for (const char *text : overflow)
expectNumber({text, strlen(text)}, NMEA_NUMBER_OUT_OF_RANGE);
expectNumber({NULL, 100}, NMEA_NUMBER_MISSING);
@@ -121,6 +125,11 @@ static void expectNumber(nmea_span_t field, nmea_number_status_t status,
int64_t coefficient, uint8_t decimalPlaces) {
nmea_decimal_t result = Adafruit_NMEA::parseDecimal(field);
assert(result.status == status);
assert(Adafruit_NMEA::validateDecimal(field) == status);
nmea_number_status_t nonnegative = status;
if (status == NMEA_NUMBER_VALID && coefficient < 0)
nonnegative = NMEA_NUMBER_OUT_OF_RANGE;
assert(Adafruit_NMEA::validateDecimal(field, false) == nonnegative);
assert(result.coefficient == coefficient);
assert(result.decimalPlaces == decimalPlaces);
}
+229
View File
@@ -9,6 +9,9 @@
#include "Adafruit_GNSS.h"
static nmea_number_status_t parseSixDigits(nmea_span_t field, uint8_t *pairs);
static bool validUnsignedByte(nmea_span_t field);
/**************************************************************************/
/*!
@brief Decode a latitude or longitude field and its hemisphere.
@@ -96,3 +99,229 @@ gnss_coordinate_t Adafruit_GNSS::parseCoordinate(nmea_span_t coordinate,
result.hemisphere = direction;
return result;
}
/**************************************************************************/
/*!
@brief Decode a complete UTC hhmmss[.fraction] field.
@param field Borrowed time field, with no terminator required.
@return Status and validated components, zeroed on failure.
Exactly six whole digits are required. A decimal point requires at least
one fractional digit; all digits are checked, with the first three retained
as milliseconds. Seconds may be 60 to represent a leap second. This checks
field ranges, not whether a leap second occurred on a particular date.
*/
/**************************************************************************/
gnss_time_t Adafruit_GNSS::parseTime(nmea_span_t field) {
gnss_time_t result = {};
uint8_t pairs[3];
nmea_span_t whole = field;
if (whole.length > 6)
whole.length = 6;
result.status = parseSixDigits(whole, pairs);
if (result.status != NMEA_NUMBER_VALID)
return result;
result.status = NMEA_NUMBER_BAD_FORMAT;
uint16_t milliseconds = 0;
uint8_t retained = 0;
if (field.length > 6) {
if (field.data[6] != '.' || field.length == 7)
return result;
for (size_t i = 7; i < field.length; i++) {
char c = field.data[i];
if (c < '0' || c > '9')
return result;
if (retained < 3) {
milliseconds = milliseconds * 10 + c - '0';
retained++;
}
}
}
while (retained < 3) {
milliseconds *= 10;
retained++;
}
result.status = NMEA_NUMBER_OUT_OF_RANGE;
if (pairs[0] > 23 || pairs[1] > 59 || pairs[2] > 60)
return result;
result.status = NMEA_NUMBER_VALID;
result.hour = pairs[0];
result.minute = pairs[1];
result.second = pairs[2];
result.millisecond = milliseconds;
return result;
}
/**************************************************************************/
/*!
@brief Decode a complete ddmmyy date field.
@param field Borrowed six-digit date, with no terminator required.
@return Status and validated components, zeroed on failure.
Validates month lengths and February 29 using year modulo four. The NMEA
field has no century; year 00 is treated as a leap year, as in 2000.
Applications requiring a full year must resolve the century separately.
*/
/**************************************************************************/
gnss_date_t Adafruit_GNSS::parseDate(nmea_span_t field) {
gnss_date_t result = {};
uint8_t pairs[3];
result.status = parseSixDigits(field, pairs);
if (result.status != NMEA_NUMBER_VALID)
return result;
result.status = NMEA_NUMBER_OUT_OF_RANGE;
uint8_t day = pairs[0], month = pairs[1], year = pairs[2];
if (!month || month > 12 || !day)
return result;
uint8_t days = 31;
if (month == 2)
days = year % 4 == 0 ? 29 : 28;
else if (month == 4 || month == 6 || month == 9 || month == 11)
days = 30;
if (day > days)
return result;
result.status = NMEA_NUMBER_VALID;
result.day = day;
result.month = month;
result.year = year;
return result;
}
// Shared syntax check for hhmmss and ddmmyy, without a copied field buffer.
static nmea_number_status_t parseSixDigits(nmea_span_t field, uint8_t *pairs) {
if (!field.data)
return NMEA_NUMBER_MISSING;
if (!field.length)
return NMEA_NUMBER_EMPTY;
if (field.length != 6)
return NMEA_NUMBER_BAD_FORMAT;
for (uint8_t i = 0; i < 3; i++) {
uint8_t tens = field.data[2 * i] - '0';
uint8_t ones = field.data[2 * i + 1] - '0';
if (tens > 9 || ones > 9)
return NMEA_NUMBER_BAD_FORMAT;
pairs[i] = tens * 10 + ones;
}
return NMEA_NUMBER_VALID;
}
/**************************************************************************/
/*!
@brief Validate standard fields before updating navigation data.
@param type Three-character sentence type, without its talker prefix.
@param fields Fields after the address comma and before the checksum.
@return Status and the first invalid or missing one-based field position.
Call after validating the enclosing NMEA frame. Checks the fields consumed
by the existing GPS decoders: GGA through geoid separation, RMC through
date, GLL through status, and GSA fix type and dilution values. Optional
tails and the satellite IDs skipped by the GPS parser are left to their
decoders. Empty fields are permitted, but a coordinate and its hemisphere
must either both be empty or both be valid for the appropriate axis.
Numeric fields must be complete representable decimals; speed, course,
and dilution values cannot be negative. Fix quality and satellite count
must fit uint8_t. Valid does not imply a position fix or populated fields.
No receiver state changes, field arrays, or heap allocation are involved.
*/
/**************************************************************************/
gnss_validation_t Adafruit_GNSS::validateNavigation(nmea_span_t type,
nmea_span_t fields) {
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;
if (sentence == UNSUPPORTED)
return result;
uint8_t required = 6, latitudeField = 1, timeField = 5;
if (sentence == GGA) {
required = 11;
latitudeField = 2;
timeField = 1;
} else if (sentence == RMC) {
required = 9;
latitudeField = 3;
timeField = 1;
} else if (sentence == GSA) {
required = 17;
timeField = 0;
}
for (uint8_t i = 1; i <= required; i++) {
nmea_span_t field = Adafruit_NMEA::nextField(fields);
result.field = i;
result.status = GNSS_SENTENCE_MISSING_FIELDS;
if (!field.data)
return result;
if (sentence != GSA && (i == latitudeField || i == latitudeField + 2)) {
nmea_span_t hemisphere = Adafruit_NMEA::nextField(fields);
if (!hemisphere.data) {
result.field = i + 1;
return result;
}
result.status = GNSS_SENTENCE_INVALID_FIELD;
if (field.length || hemisphere.length) {
gnss_coordinate_t coordinate = parseCoordinate(field, hemisphere);
bool latitude = i == latitudeField;
if (coordinate.status != NMEA_NUMBER_VALID ||
(latitude && coordinate.hemisphere != 'N' &&
coordinate.hemisphere != 'S') ||
(!latitude && coordinate.hemisphere != 'E' &&
coordinate.hemisphere != 'W'))
return result;
}
i++;
continue;
}
if (!field.length)
continue;
result.status = GNSS_SENTENCE_INVALID_FIELD;
if (sentence == GSA && (i == 1 || (i >= 3 && i <= 14))) {
continue; // Selection mode and satellite IDs are not decoded by GPS.
} else if (i == timeField) {
if (parseTime(field).status != NMEA_NUMBER_VALID)
return result;
} else if (sentence == RMC && i == 9) {
if (parseDate(field).status != NMEA_NUMBER_VALID)
return result;
} else if ((sentence == RMC && i == 2) || (sentence == GLL && i == 6)) {
if (field.length != 1 || (field.data[0] != 'A' && field.data[0] != 'V'))
return result;
} else if ((sentence == GGA && (i == 6 || i == 7)) ||
(sentence == GSA && i == 2)) {
if (!validUnsignedByte(field))
return result;
} else if (sentence == GGA && i == 10) {
if (field.length != 1 || field.data[0] != 'M')
return result;
} else {
bool signedValue = sentence == GGA && (i == 9 || i == 11);
if (Adafruit_NMEA::validateDecimal(field, signedValue) !=
NMEA_NUMBER_VALID)
return result;
}
}
result.status = GNSS_SENTENCE_VALID;
result.field = 0;
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;
for (size_t i = 0; i < field.length; i++) {
char c = field.data[i];
if (c < '0' || c > '9')
return false;
value = value * 10 + c - '0';
if (value > UINT8_MAX)
return false;
}
return true;
}
+45
View File
@@ -25,6 +25,37 @@ typedef struct {
char hemisphere; ///< Exactly N, S, E, or W on success.
} gnss_coordinate_t;
/// Decoded UTC time; all components are zero on failure.
typedef struct {
nmea_number_status_t status; ///< Result of decoding the field.
uint8_t hour; ///< UTC hour, 0 through 23.
uint8_t minute; ///< Minute, 0 through 59.
uint8_t second; ///< Second, 0 through 60 for a leap second.
uint16_t millisecond; ///< Fraction truncated to milliseconds, 0 to 999.
} gnss_time_t;
/// Decoded NMEA date; no century is inferred. Components are zero on failure.
typedef struct {
nmea_number_status_t status; ///< Result of decoding the field.
uint8_t day; ///< Day of month, 1 through 31.
uint8_t month; ///< Month, 1 through 12.
uint8_t year; ///< Two-digit year, 0 through 99.
} gnss_date_t;
/// Result of validating fields used by a standard navigation decoder.
typedef enum : uint8_t {
GNSS_SENTENCE_VALID, ///< Supported fields are valid or empty.
GNSS_SENTENCE_UNSUPPORTED, ///< This validator does not handle the type.
GNSS_SENTENCE_MISSING_FIELDS, ///< A required field position is absent.
GNSS_SENTENCE_INVALID_FIELD ///< A populated field has invalid content.
} gnss_sentence_status_t;
/// Validation result; coordinate-pair errors identify the coordinate field.
typedef struct {
gnss_sentence_status_t status; ///< Result of navigation field validation.
uint8_t field; ///< One-based error field, or zero for valid/unsupported.
} gnss_validation_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.
@@ -33,6 +64,20 @@ class Adafruit_GNSS {
public:
static gnss_coordinate_t parseCoordinate(nmea_span_t coordinate,
nmea_span_t hemisphere);
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:
/// Sentence kinds handled by the navigation validator.
enum {
UNSUPPORTED, ///< Sentence has no standard navigation validation.
GGA, ///< Fix time, position, and altitude.
RMC, ///< Recommended minimum navigation data.
GLL, ///< Geographic position and time.
GSA ///< Fix type and dilution of precision.
};
};
#endif // ADAFRUIT_GNSS_H
+1
View File
@@ -250,6 +250,7 @@ private:
nmea_float_t *angle = NULL, int32_t *angle_fixed = NULL,
char *dir = NULL);
char *parseStr(char *buff, char *p, int n);
char *parseCoordinates(char *p);
bool parseTime(char *);
bool parseFix(char *);
bool parseAntenna(char *);
+85 -59
View File
@@ -313,6 +313,57 @@ nmea_span_t Adafruit_NMEA::nextField(nmea_span_t &remaining) {
return field;
}
static nmea_number_status_t scanDecimal(nmea_span_t field, uint8_t &places,
bool &negative) {
if (!field.data)
return NMEA_NUMBER_MISSING;
if (!field.length)
return NMEA_NUMBER_EMPTY;
size_t start = 0;
negative = field.data[0] == '-';
if (negative || field.data[0] == '+')
start = 1;
bool decimalPoint = false, hasDigit = false, overflow = false;
uint8_t significant = 0;
int8_t comparison = 0;
places = 0;
// Compare significant digits with INT64_MAX's magnitude. This also accepts
// INT64_MIN's extra final digit without requiring any 64-bit arithmetic.
const char *limit = "9223372036854775807";
for (size_t i = start; i < field.length; i++) {
char c = field.data[i];
if (c == '.' && !decimalPoint) {
decimalPoint = true;
continue;
}
if (c < '0' || c > '9')
return NMEA_NUMBER_BAD_FORMAT;
hasDigit = true;
if (decimalPoint) {
if (places == UINT8_MAX)
overflow = true;
else
places++;
}
if (significant || c != '0') {
if (significant < 20)
significant++;
if (significant <= 19 && !comparison) {
char bound =
significant == 19 && negative ? '8' : limit[significant - 1];
if (c != bound)
comparison = c < bound ? -1 : 1;
}
}
}
if (!hasDigit)
return NMEA_NUMBER_BAD_FORMAT;
if (overflow || significant > 19 || (significant == 19 && comparison > 0))
return NMEA_NUMBER_OUT_OF_RANGE;
negative = negative && significant; // Negative zero remains nonnegative.
return NMEA_NUMBER_VALID;
}
/**************************************************************************/
/*!
@brief Convert a complete field to an exact signed decimal value.
@@ -330,72 +381,47 @@ nmea_span_t Adafruit_NMEA::nextField(nmea_span_t &remaining) {
/**************************************************************************/
nmea_decimal_t Adafruit_NMEA::parseDecimal(nmea_span_t field) {
nmea_decimal_t result = {NMEA_NUMBER_BAD_FORMAT, 0, 0};
if (!field.data) {
result.status = NMEA_NUMBER_MISSING;
return result;
}
if (!field.length) {
result.status = NMEA_NUMBER_EMPTY;
return result;
}
size_t start = 0;
uint8_t places = 0;
bool negative = false;
if (field.data[0] == '-' || field.data[0] == '+') {
negative = field.data[0] == '-';
start = 1;
}
// Accumulate negatively: INT64_MIN has no positive int64_t counterpart.
int64_t limit = -INT64_MAX;
if (negative)
limit = INT64_MIN;
int64_t cutoff = limit / 10;
uint8_t lastDigitLimit = (uint8_t)(-(limit % 10));
result.status = scanDecimal(field, places, negative);
if (result.status != NMEA_NUMBER_VALID)
return result;
// The complete field is already checked, so accumulation cannot overflow.
// Accumulate negatively because INT64_MIN has no positive counterpart.
int64_t coefficient = 0;
uint8_t decimalPlaces = 0;
bool decimalPoint = false;
bool hasDigit = false;
bool overflow = false;
for (size_t i = start; i < field.length; i++) {
for (size_t i = 0; i < field.length; i++) {
char c = field.data[i];
if (c == '.' && !decimalPoint) {
decimalPoint = true;
continue;
}
if (c < '0' || c > '9')
return result;
hasDigit = true;
uint8_t digit = c - '0';
if (decimalPoint) {
if (decimalPlaces == UINT8_MAX)
overflow = true;
else
decimalPlaces++;
}
if (!overflow) {
if (coefficient < cutoff ||
(coefficient == cutoff && digit > lastDigitLimit))
overflow = true;
else
coefficient = coefficient * 10 - digit;
}
if (c >= '0' && c <= '9')
coefficient = coefficient * 10 - (c - '0');
}
if (!hasDigit)
return result;
if (overflow) {
result.status = NMEA_NUMBER_OUT_OF_RANGE;
return result;
}
result.status = NMEA_NUMBER_VALID;
result.coefficient = coefficient;
if (!negative)
result.coefficient = -coefficient;
result.decimalPlaces = decimalPlaces;
result.coefficient = negative ? coefficient : -coefficient;
result.decimalPlaces = places;
return result;
}
/**************************************************************************/
/*!
@brief Check an exact decimal without constructing its numeric value.
@param field Borrowed readable text, with no NUL terminator required.
@param allowNegative Whether to accept negative nonzero values.
@return The same syntax and range status as parseDecimal(). Negative
nonzero values give OUT_OF_RANGE when allowNegative is false.
Retains parseDecimal's signed 64-bit coefficient and fractional digit
limits, including malformed-syntax precedence. Uses no 64-bit arithmetic
or allocation. Negative zero is accepted in either mode.
*/
/**************************************************************************/
nmea_number_status_t Adafruit_NMEA::validateDecimal(nmea_span_t field,
bool allowNegative) {
uint8_t places = 0;
bool negative = false;
nmea_number_status_t status = scanDecimal(field, places, negative);
if (status == NMEA_NUMBER_VALID && negative && !allowNegative)
return NMEA_NUMBER_OUT_OF_RANGE;
return status;
}
/**************************************************************************/
/*!
@brief Build a checksummed command in caller-provided storage.
+2
View File
@@ -88,6 +88,8 @@ public:
static nmea_sentence_t validate(const char *data, size_t length);
static nmea_span_t nextField(nmea_span_t &remaining);
static nmea_decimal_t parseDecimal(nmea_span_t field);
static nmea_number_status_t validateDecimal(nmea_span_t field,
bool allowNegative = true);
static size_t buildCommand(char *output, size_t capacity, const char *body,
size_t bodyLength);
+54 -57
View File
@@ -87,26 +87,25 @@ bool Adafruit_GPS::parse(char *nmea) {
// non-destructive
p = strchr(p, ',') + 1; // Skip to char after the next comma, then check.
// Validate every field consumed by a standard navigation decoder before
// changing any fix data. The enclosing frame has already passed check().
nmea_span_t type = {thisSentence, strlen(thisSentence)};
nmea_span_t dataFields = {p, (size_t)(strchr(p, '*') - p)};
gnss_validation_t validation =
Adafruit_GNSS::validateNavigation(type, dataFields);
if (validation.status != GNSS_SENTENCE_VALID &&
validation.status != GNSS_SENTENCE_UNSUPPORTED)
return false;
// This may look inefficient, but an M0 will get down the list in about 1 us /
// strcmp()! Put the GPS sentences from Adafruit_GPS at the top to make
// pruning excess code easier. Otherwise, keep them alphabetical for ease of
// reading.
if (!strcmp(thisSentence, "GGA")) { //************************************GGA
if (fields < 11)
return false;
// Adafruit from Actisense NGW-1 from SH CP150C
parseTime(p);
p = strchr(p, ',') + 1; // parse time with specialized function
// parse out both latitude and direction, then go to next field, or fail
if (parseCoord(p, &latitudeDegrees, &latitude, &latitude_fixed, &lat))
newDataValue(NMEA_LAT, latitudeDegrees);
p = strchr(p, ',') + 1;
p = strchr(p, ',') + 1;
// parse out both longitude and direction, then go to next field, or fail
if (parseCoord(p, &longitudeDegrees, &longitude, &longitude_fixed, &lon))
newDataValue(NMEA_LON, longitudeDegrees);
p = strchr(p, ',') + 1;
p = strchr(p, ',') + 1;
p = parseCoordinates(p);
if (!isEmpty(p)) { // if it's a , (or a * at end of sentence) the value is
// not included
fixquality = atoi(p); // needs additional processing
@@ -132,23 +131,12 @@ bool Adafruit_GPS::parse(char *nmea) {
geoidheight = atof(p); // skip the rest
} else if (!strcmp(thisSentence, "RMC")) { //*****************************RMC
if (fields < 9)
return false;
// in Adafruit from Actisense NGW-1 from SH CP150C
parseTime(p);
p = strchr(p, ',') + 1;
parseFix(p);
p = strchr(p, ',') + 1;
// parse out both latitude and direction, then go to next field, or fail
if (parseCoord(p, &latitudeDegrees, &latitude, &latitude_fixed, &lat))
newDataValue(NMEA_LAT, latitudeDegrees);
p = strchr(p, ',') + 1;
p = strchr(p, ',') + 1;
// parse out both longitude and direction, then go to next field, or fail
if (parseCoord(p, &longitudeDegrees, &longitude, &longitude_fixed, &lon))
newDataValue(NMEA_LON, longitudeDegrees);
p = strchr(p, ',') + 1;
p = strchr(p, ',') + 1;
p = parseCoordinates(p);
if (!isEmpty(p))
newDataValue(NMEA_SOG, speed = atof(p));
p = strchr(p, ',') + 1;
@@ -156,34 +144,22 @@ bool Adafruit_GPS::parse(char *nmea) {
newDataValue(NMEA_COG, angle = atof(p));
p = strchr(p, ',') + 1;
if (!isEmpty(p)) {
uint32_t fulldate = atof(p);
day = fulldate / 10000;
month = (fulldate % 10000) / 100;
year = (fulldate % 100);
// RMC dates have already passed full-field validation above.
gnss_date_t date = Adafruit_GNSS::parseDate({p, 6});
day = date.day;
month = date.month;
year = date.year;
lastDate = sentTime;
} // skip the rest
} else if (!strcmp(thisSentence, "GLL")) { //*****************************GLL
if (fields < 6)
return false;
// in Adafruit from Actisense NGW-1 from SH CP150C
// parse out both latitude and direction, then go to next field, or fail
if (parseCoord(p, &latitudeDegrees, &latitude, &latitude_fixed, &lat))
newDataValue(NMEA_LAT, latitudeDegrees);
p = strchr(p, ',') + 1;
p = strchr(p, ',') + 1;
// parse out both longitude and direction, then go to next field, or fail
if (parseCoord(p, &longitudeDegrees, &longitude, &longitude_fixed, &lon))
newDataValue(NMEA_LON, longitudeDegrees);
p = strchr(p, ',') + 1;
p = strchr(p, ',') + 1;
p = parseCoordinates(p);
parseTime(p);
p = strchr(p, ',') + 1;
parseFix(p); // skip the rest
} else if (!strcmp(thisSentence, "GSA")) { //*****************************GSA
if (fields < 17)
return false;
// in Adafruit from Actisense NGW-1
p = strchr(p, ',') + 1; // skip selection mode
if (!isEmpty(p))
@@ -781,6 +757,28 @@ bool Adafruit_GPS::parseCoord(char *pStart, nmea_float_t *angleDegrees,
return true;
}
/**************************************************************************/
/*!
@brief Update the adjacent latitude and longitude fields of a sentence.
@param p First coordinate field in an already validated GGA, RMC, or GLL.
@return Field following longitude and its hemisphere.
The caller must validate all four fields before calling. Empty coordinate
pairs leave their previous values unchanged. Use the same exact conversion
and data update path for each navigation sentence.
*/
/**************************************************************************/
char *Adafruit_GPS::parseCoordinates(char *p) {
if (parseCoord(p, &latitudeDegrees, &latitude, &latitude_fixed, &lat))
newDataValue(NMEA_LAT, latitudeDegrees);
p = strchr(p, ',') + 1;
p = strchr(p, ',') + 1;
if (parseCoord(p, &longitudeDegrees, &longitude, &longitude_fixed, &lon))
newDataValue(NMEA_LON, longitudeDegrees);
p = strchr(p, ',') + 1;
return strchr(p, ',') + 1;
}
/**************************************************************************/
/*!
@brief Parse a string token from pointer p to the next comma, asterisk
@@ -816,21 +814,20 @@ char *Adafruit_GPS::parseStr(char *buff, char *p, int n) {
*/
/**************************************************************************/
bool Adafruit_GPS::parseTime(char *p) {
if (!isEmpty(p)) { // get time
uint32_t time = atol(p);
hour = time / 10000;
minute = (time % 10000) / 100;
seconds = (time % 100);
char *dec = strchr(p, '.');
char *comstar = min(strchr(p, ','), strchr(p, '*'));
if (dec != NULL && comstar != NULL && dec < comstar)
milliseconds = atof(dec) * 1000;
else
milliseconds = 0;
lastTime = sentTime;
return true;
}
return false;
if (!p)
return false;
char *end = p;
while (*end && *end != ',' && *end != '*')
end++;
gnss_time_t time = Adafruit_GNSS::parseTime({p, (size_t)(end - p)});
if (time.status != NMEA_NUMBER_VALID)
return false;
hour = time.hour;
minute = time.minute;
seconds = time.second;
milliseconds = time.millisecond;
lastTime = sentTime;
return true;
}
/**************************************************************************/