mirror of
https://github.com/adafruit/Adafruit_GPS.git
synced 2026-10-26 22:10:17 +00:00
Merge pull request #193 from ladyada-eagleclaw/gps-navigation-validation
Validate navigation fields before updating GPS data
This commit is contained in:
11 files changed
+746
-126
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user