From 1eab555a50448c4fc555b0abaa1dea823b7d34da Mon Sep 17 00:00:00 2001 From: ladyada Date: Tue, 15 Sep 2026 20:58:12 -0400 Subject: [PATCH 1/2] Validate navigation fields before updating GPS data Share bounded UTC time, date, and standard navigation validation in Adafruit_GNSS. Reject malformed populated GGA, RMC, GLL, and GSA fields before changing stored values or freshness timestamps, while preserving empty-field behavior and the high-precision coordinate path. Co-authored-by: Limor Fried --- .../tests/gnss_navigation/gnss_navigation.cpp | 100 ++++++++ .../tests/gnss_time_date/gnss_time_date.cpp | 85 +++++++ .../navigation_state/navigation_state.ino | 120 ++++++++++ src/Adafruit_GNSS.cpp | 226 ++++++++++++++++++ src/Adafruit_GNSS.h | 35 +++ src/NMEA_parse.cpp | 48 ++-- 6 files changed, 595 insertions(+), 19 deletions(-) create mode 100644 extras/tests/gnss_navigation/gnss_navigation.cpp create mode 100644 extras/tests/gnss_time_date/gnss_time_date.cpp create mode 100644 extras/tests/navigation_state/navigation_state.ino diff --git a/extras/tests/gnss_navigation/gnss_navigation.cpp b/extras/tests/gnss_navigation/gnss_navigation.cpp new file mode 100644 index 0000000..2fec355 --- /dev/null +++ b/extras/tests/gnss_navigation/gnss_navigation.cpp @@ -0,0 +1,100 @@ +#include "Adafruit_GNSS.h" +#include +#include +#include +#include + +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; +} diff --git a/extras/tests/gnss_time_date/gnss_time_date.cpp b/extras/tests/gnss_time_date/gnss_time_date.cpp new file mode 100644 index 0000000..b1b46c2 --- /dev/null +++ b/extras/tests/gnss_time_date/gnss_time_date.cpp @@ -0,0 +1,85 @@ +#include "Adafruit_GNSS.h" +#include +#include +#include + +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; +} diff --git a/extras/tests/navigation_state/navigation_state.ino b/extras/tests/navigation_state/navigation_state.ino new file mode 100644 index 0000000..4e205f8 --- /dev/null +++ b/extras/tests/navigation_state/navigation_state.ino @@ -0,0 +1,120 @@ +#include +#include +#include +#define private public +#include +#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); +} diff --git a/src/Adafruit_GNSS.cpp b/src/Adafruit_GNSS.cpp index 71b3950..303440e 100644 --- a/src/Adafruit_GNSS.cpp +++ b/src/Adafruit_GNSS.cpp @@ -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,226 @@ 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 < 6; i++) { + char c = field.data[i]; + if (c < '0' || c > '9') + return NMEA_NUMBER_BAD_FORMAT; + if (i % 2 == 0) + pairs[i / 2] = (c - '0') * 10; + else + pairs[i / 2] += c - '0'; + } + 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; + bool gga = type.data[0] == 'G' && type.data[1] == 'G' && type.data[2] == 'A'; + bool rmc = type.data[0] == 'R' && type.data[1] == 'M' && type.data[2] == 'C'; + bool gll = type.data[0] == 'G' && type.data[1] == 'L' && type.data[2] == 'L'; + bool gsa = type.data[0] == 'G' && type.data[1] == 'S' && type.data[2] == 'A'; + if (!gga && !rmc && !gll && !gsa) + return result; + uint8_t required = 6, latitudeField = 1, timeField = 5; + if (gga) { + required = 11; + latitudeField = 2; + timeField = 1; + } else if (rmc) { + required = 9; + latitudeField = 3; + timeField = 1; + } else if (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 (!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 (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 (rmc && i == 9) { + if (parseDate(field).status != NMEA_NUMBER_VALID) + return result; + } else if ((rmc && i == 2) || (gll && i == 6)) { + if (field.length != 1 || (field.data[0] != 'A' && field.data[0] != 'V')) + return result; + } else if ((gga && (i == 6 || i == 7)) || (gsa && i == 2)) { + if (!validUnsignedByte(field)) + return result; + } else if (gga && i == 10) { + if (field.length != 1 || field.data[0] != 'M') + return result; + } else { + nmea_decimal_t number = Adafruit_NMEA::parseDecimal(field); + bool signedValue = gga && (i == 9 || i == 11); + if (number.status != NMEA_NUMBER_VALID || + (!signedValue && number.coefficient < 0)) + 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; +} diff --git a/src/Adafruit_GNSS.h b/src/Adafruit_GNSS.h index a2a4dcf..85c81c0 100644 --- a/src/Adafruit_GNSS.h +++ b/src/Adafruit_GNSS.h @@ -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,10 @@ 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); }; #endif // ADAFRUIT_GNSS_H diff --git a/src/NMEA_parse.cpp b/src/NMEA_parse.cpp index 3d4bba7..27263e2 100644 --- a/src/NMEA_parse.cpp +++ b/src/NMEA_parse.cpp @@ -87,6 +87,16 @@ 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 @@ -156,10 +166,11 @@ 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 @@ -816,21 +827,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; } /**************************************************************************/ From 910ded451a3b8ac10f377c9b88963aded74901f0 Mon Sep 17 00:00:00 2001 From: ladyada Date: Tue, 15 Sep 2026 21:57:40 -0400 Subject: [PATCH 2/2] Keep validated navigation parsing within AVR logger flash limits Co-authored-by: Limor Fried --- examples/shield_sdlog/shield_sdlog.ino | 24 +-- .../tests/nmea_utilities/nmea_utilities.cpp | 11 +- src/Adafruit_GNSS.cpp | 53 ++++--- src/Adafruit_GNSS.h | 10 ++ src/Adafruit_GPS.h | 1 + src/Adafruit_NMEA.cpp | 144 +++++++++++------- src/Adafruit_NMEA.h | 2 + src/NMEA_parse.cpp | 63 +++----- 8 files changed, 176 insertions(+), 132 deletions(-) diff --git a/examples/shield_sdlog/shield_sdlog.ino b/examples/shield_sdlog/shield_sdlog.ino index 5f03a6c..a9bcb0c 100644 --- a/examples/shield_sdlog/shield_sdlog.ino +++ b/examples/shield_sdlog/shield_sdlog.ino @@ -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(); } } diff --git a/extras/tests/nmea_utilities/nmea_utilities.cpp b/extras/tests/nmea_utilities/nmea_utilities.cpp index 9648c08..b3829ca 100644 --- a/extras/tests/nmea_utilities/nmea_utilities.cpp +++ b/extras/tests/nmea_utilities/nmea_utilities.cpp @@ -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); } diff --git a/src/Adafruit_GNSS.cpp b/src/Adafruit_GNSS.cpp index 303440e..fccdac4 100644 --- a/src/Adafruit_GNSS.cpp +++ b/src/Adafruit_GNSS.cpp @@ -195,14 +195,12 @@ static nmea_number_status_t parseSixDigits(nmea_span_t field, uint8_t *pairs) { return NMEA_NUMBER_EMPTY; if (field.length != 6) return NMEA_NUMBER_BAD_FORMAT; - for (uint8_t i = 0; i < 6; i++) { - char c = field.data[i]; - if (c < '0' || c > '9') + 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; - if (i % 2 == 0) - pairs[i / 2] = (c - '0') * 10; - else - pairs[i / 2] += c - '0'; + pairs[i] = tens * 10 + ones; } return NMEA_NUMBER_VALID; } @@ -231,22 +229,27 @@ 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; - bool gga = type.data[0] == 'G' && type.data[1] == 'G' && type.data[2] == 'A'; - bool rmc = type.data[0] == 'R' && type.data[1] == 'M' && type.data[2] == 'C'; - bool gll = type.data[0] == 'G' && type.data[1] == 'L' && type.data[2] == 'L'; - bool gsa = type.data[0] == 'G' && type.data[1] == 'S' && type.data[2] == 'A'; - if (!gga && !rmc && !gll && !gsa) + 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 (gga) { + if (sentence == GGA) { required = 11; latitudeField = 2; timeField = 1; - } else if (rmc) { + } else if (sentence == RMC) { required = 9; latitudeField = 3; timeField = 1; - } else if (gsa) { + } else if (sentence == GSA) { required = 17; timeField = 0; } @@ -256,7 +259,7 @@ gnss_validation_t Adafruit_GNSS::validateNavigation(nmea_span_t type, result.status = GNSS_SENTENCE_MISSING_FIELDS; if (!field.data) return result; - if (!gsa && (i == latitudeField || i == latitudeField + 2)) { + if (sentence != GSA && (i == latitudeField || i == latitudeField + 2)) { nmea_span_t hemisphere = Adafruit_NMEA::nextField(fields); if (!hemisphere.data) { result.field = i + 1; @@ -279,28 +282,28 @@ gnss_validation_t Adafruit_GNSS::validateNavigation(nmea_span_t type, if (!field.length) continue; result.status = GNSS_SENTENCE_INVALID_FIELD; - if (gsa && (i == 1 || (i >= 3 && i <= 14))) { + 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 (rmc && i == 9) { + } else if (sentence == RMC && i == 9) { if (parseDate(field).status != NMEA_NUMBER_VALID) return result; - } else if ((rmc && i == 2) || (gll && i == 6)) { + } 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 ((gga && (i == 6 || i == 7)) || (gsa && i == 2)) { + } else if ((sentence == GGA && (i == 6 || i == 7)) || + (sentence == GSA && i == 2)) { if (!validUnsignedByte(field)) return result; - } else if (gga && i == 10) { + } else if (sentence == GGA && i == 10) { if (field.length != 1 || field.data[0] != 'M') return result; } else { - nmea_decimal_t number = Adafruit_NMEA::parseDecimal(field); - bool signedValue = gga && (i == 9 || i == 11); - if (number.status != NMEA_NUMBER_VALID || - (!signedValue && number.coefficient < 0)) + bool signedValue = sentence == GGA && (i == 9 || i == 11); + if (Adafruit_NMEA::validateDecimal(field, signedValue) != + NMEA_NUMBER_VALID) return result; } } diff --git a/src/Adafruit_GNSS.h b/src/Adafruit_GNSS.h index 85c81c0..884ee01 100644 --- a/src/Adafruit_GNSS.h +++ b/src/Adafruit_GNSS.h @@ -68,6 +68,16 @@ public: 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 diff --git a/src/Adafruit_GPS.h b/src/Adafruit_GPS.h index b2aa397..35b5a66 100644 --- a/src/Adafruit_GPS.h +++ b/src/Adafruit_GPS.h @@ -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 *); diff --git a/src/Adafruit_NMEA.cpp b/src/Adafruit_NMEA.cpp index 17e623d..c90ffcc 100644 --- a/src/Adafruit_NMEA.cpp +++ b/src/Adafruit_NMEA.cpp @@ -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. diff --git a/src/Adafruit_NMEA.h b/src/Adafruit_NMEA.h index 76900b3..4891cdb 100644 --- a/src/Adafruit_NMEA.h +++ b/src/Adafruit_NMEA.h @@ -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); diff --git a/src/NMEA_parse.cpp b/src/NMEA_parse.cpp index 27263e2..0431bdc 100644 --- a/src/NMEA_parse.cpp +++ b/src/NMEA_parse.cpp @@ -102,21 +102,10 @@ bool Adafruit_GPS::parse(char *nmea) { // 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 @@ -142,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; @@ -175,26 +153,13 @@ bool Adafruit_GPS::parse(char *nmea) { } // 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)) @@ -792,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