Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gps parse improvements #156

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
395 changes: 395 additions & 0 deletions extras/NMEA_Parse_Test/NMEA_Parse_Test.ino
@@ -0,0 +1,395 @@
#include <Adafruit_GPS.h>
//
// Test code for the Adafruit GPS library.
//
// Specifically this code was used to test a refactoring of the library to make
// it more robust while ensuring that the parsing of correct sentences remained
// unchanged.
//
// This code will crash when used with the old parsing implementation unless
// TEST_INVALID_SENTENCES is undefined.

Adafruit_GPS GPS;

void setup() {
Serial.begin(115200);
delay(3000); // Wait a bit for USB serial to enumerate on some devices
Serial.println("Adafruit GPS parse test.");
// Use 0 as a seed, since we don't actually want truly random numbers
randomSeed(0);
}

// Only set this if we are testing the new library, otherwise
// it guarantees a crash accessing and invalid pointer
#define TEST_INVALID_SENTENCES

// How many generated sentances to make
#define GENERATE_COUNT 100

const char* GPS_SENTENCES_REAL[] = {
// These came from my actual Ultimate GPS module
"$GPGGA,200344.000,1329.6513,N,08923.3574,W,2,11,0.88,40.3,M,-0.7,M,0000,0000*5C",
"$GPRMC,200344.000,A,1329.6513,N,08923.3574,W,0.01,353.13,080324,,,D*7E",
"$GPGGA,200345.000,1329.6513,N,08923.3574,W,2,11,0.88,40.3,M,-0.7,M,0000,0000*5D",
"$GPRMC,200345.000,A,1329.6513,N,08923.3574,W,0.02,24.34,080324,,,D*4A",
"$GPGGA,200346.000,1329.6513,N,08923.3574,W,2,11,0.88,40.3,M,-0.7,M,0000,0000*5E",
"$GPRMC,200346.000,A,1329.6513,N,08923.3574,W,0.01,353.59,080324,,,D*72",
"$GPGGA,200347.000,1329.6513,N,08923.3574,W,2,11,0.88,40.3,M,-0.7,M,0000,0000*5F",
"$GPRMC,200347.000,A,1329.6513,N,08923.3574,W,0.00,99.87,080324,,,D*44",
"$GPGGA,200348.000,1329.6513,N,08923.3574,W,2,11,0.88,40.3,M,-0.7,M,0000,0000*50",
"$GPRMC,200348.000,A,1329.6513,N,08923.3574,W,0.01,179.99,080324,,,D*7A",
"$GPGGA,200349.000,1329.6513,N,08923.3574,W,2,11,0.88,40.3,M,-0.7,M,0000,0000*51",
"$GPRMC,200349.000,A,1329.6513,N,08923.3574,W,0.01,136.30,080324,,,D*73",
"$GPGGA,200350.000,1329.6513,N,08923.3574,W,2,11,0.88,40.3,M,-0.7,M,0000,0000*59",
"$GPRMC,200350.000,A,1329.6513,N,08923.3574,W,0.01,174.78,080324,,,D*71",
"$GPVTG,128.54,T,,M,0.21,N,0.39,K,D*3B",

// These came from the output example on this Github project:
// https://github.com/luk-kop/nmea-gps-emulator
"$GPGGA,173124.00,5430.000,N,01921.029,E,1,09,0.92,15.2,M,32.5,M,,*6C",
"$GPGSA,A,3,22,11,27,01,03,02,10,21,19,,,,1.56,0.92,1.25*02",
"$GPGSV,4,1,15,26,25,138,53,16,25,091,67,01,51,238,77,02,45,085,41*79",
"$GPGSV,4,2,15,03,38,312,01,30,68,187,37,11,22,049,44,09,67,076,71*77",
"$GPGSV,4,3,15,10,14,177,12,19,86,235,37,21,84,343,95,22,77,040,66*79",
"$GPGSV,4,4,15,08,50,177,60,06,81,336,46,27,63,209,83*4C",
"$GPGLL,5430.000,N,01921.029,E,173124.000,A,A*59",
"$GPRMC,173124.000,A,5430.000,N,01921.029,E,10.500,90.0,051121,,,A*65",
"$GPHDT,90.0,T*0C",
"$GPVTG,90.0,T,,M,10.5,N,19.4,K*51",
"$GPZDA,173124.000,05,11,2021,0,0*50",

// Sample from (https://learn.adafruit.com/adafruit-ultimate-gps-featherwing/antenna-options) for PGTOP"
"$PGTOP,11,3*6F",
"$PGTOP,11,2*6E",
};

// These came from my GPS when I wasn't reading the buffer quickly enough
const char* GPS_SENTENCES_INVALID[] = {
"$GPGGA,215937.5.1022,W,0.34,222.98,280124,,,D*73"
};

// This is the list of sentences types the library knows how to build
const char* BUILT_SENTENCES[] = {"GGA", "GLL", "RMC", "HDM",
"HDT", "MWV", "RMB", "TXT",
"VHW", "VPW", "WCV" };

// This is the list of full sentences created by hand to test the parser.
// These are sentences that are understood by the parsers, but are not built
// by the build() function, and not otherwise covered by any of the the REAL
// sentences declared above.
//
// These were generated by ChatGPT, so they might be nonsense. I expect
// the checksum is wrong.
const char* SYNTHETIC_TEST_SENTENCES[] = {
// DBT - Depth Below Transducer
"$IIDBT,030.5,f,009.3,M,005.0,F*18",
// MDA - Meteorological Composite
"$WIMDA,30.03,I,1.018,B,25.0,C,,,50.0,,2.5,C,1018.2,,*46",
// MTW - Water Temperature
"$IIMTW,15.6,C*11",
// VLW - Distance Traveled through Water
"$IIVLW,01234.56,N,000.0,N*7A",
// VWR - Wind Speed and Angle
"$IIVWR,045.,L,010.5,N,005.4,M,019.4,K*6F",
// XTE - Cross-Track Error
"$GPXTE,A,A,0.10,L,N*6F"
};

void loop() {
char *input;
char output[200];
char invalid[200];
char source[3];
char sentence[4];
boolean rval;
int array_length;

Serial.println("********** REAL SENTENCES **********");
// Test known valid sentences (from an actual GPS)
// We compare these manually, as the exact fromat from our builder will differ
array_length = sizeof(GPS_SENTENCES_REAL) / sizeof(GPS_SENTENCES_REAL[0]);
for (int i = 0; i < array_length; i++) {
input = (char*)GPS_SENTENCES_REAL[i];
rval = GPS.parse(input);
source[0] = input[1]; source[1] = input[2]; source[2] = 0;
sentence[0] = input[3]; sentence[1] = input[4]; sentence[2] = input[5]; sentence[3] = 0;

Serial.println("Input: " + String(input) + " - Parsed: " + (rval?"Ok":"Error"));
if (!rval) {
Serial.println();
} else if (GPS.build(output, source, sentence)) {
Serial.println("Output: " + String(output));
} else {
Serial.println("No build implementation.\n");
}
}

Serial.println("********** SYNTHETIC SENTENCES **********");
// This is the list of full sentences created by hand to test the parser.
// In these cases there isn't a readily available example, and the library doesn't
// build these types of sentances
array_length = sizeof(SYNTHETIC_TEST_SENTENCES) / sizeof(SYNTHETIC_TEST_SENTENCES[0]);
for (int i = 0; i < array_length; i++) {
input = (char*)SYNTHETIC_TEST_SENTENCES[i];
rval = GPS.parse(input);
source[0] = input[1]; source[1] = input[2]; source[2] = 0;
sentence[0] = input[3]; sentence[1] = input[4]; sentence[2] = input[5]; sentence[3] = 0;

Serial.println("Input: " + String(input) + " - Parsed: " + (rval?"Ok":"Error"));
}
Serial.println();

Serial.println("********** BUILT SENTENCES (generated) **********");
// Test sentences that this library knows how to build. Sadly, it doesn't seem to know how
// to build everything it knows how to parse
array_length = // Subtract one because of a weird terminator string
sizeof(BUILT_SENTENCES) / sizeof(BUILT_SENTENCES[0]);
uint32_t inputHash, outputHash;
bool match;
for (int j = 0; j < GENERATE_COUNT; j++) {
for (int i = 0; i < array_length; i++) {
updateGPS(); // Update GPS data with arbitrary values
inputHash = hash(GPS); // Get a hash of the GPS data
if (GPS.build(output, "GP", BUILT_SENTENCES[i])) {
output[strlen(output) - 1] = 0; // Remove carriage-return
Serial.print("Build output: " + String(output));
rval = GPS.parse(output);
Serial.print(" - Parsed: " + String(rval?"Ok":"Error"));
outputHash = hash(GPS); // Get a hash of the GPS data
// The input and output hash should match.
//
// NOTE:That the TXT message hashes will not match the first time,
// because the build code for the TXT message adds some random hardcoded
// test data.
match = (inputHash == outputHash);
Serial.println(", Hash Match?: " + String(match?"Yes":"No"));
} else {
Serial.println("Build Error: " + String(BUILT_SENTENCES[i]));
}
}
}
Serial.println();

#ifdef TEST_INVALID_SENTENCES
Serial.println("********** INVALID SENTENCES (prefabbed) **********");
// Test invalid sentences (ideally some with valid checksums)
// We compare these manually, as the exact fromat from our builder will differ
array_length = sizeof(GPS_SENTENCES_INVALID) / sizeof(GPS_SENTENCES_INVALID[0]);
for (int i = 0; i < array_length; i++) {
input = (char*)GPS_SENTENCES_INVALID[i];
rval = GPS.parse(input);
source[0] = input[1]; source[1] = input[2]; source[2] = 0;
sentence[0] = input[3]; sentence[1] = input[4]; sentence[2] = input[5]; sentence[3] = 0;

Serial.println("Input: " + String(input) + " - Parsed: " + (rval?"Ok":"Error"));
if (!rval) {
Serial.println();
} else if (GPS.build(output, source, sentence)) {
Serial.println("Output: " + String(output));
} else {
Serial.println("No build implementation.\n");
}
}

Serial.println("********** INVALID SENTENCES (generated) **********");
// Test sentences that this library knows how to build. Sadly, it doesn't seem to know how
// to build everything it knows how to parse
array_length = // Subtract one because of a weird terminator string
sizeof(BUILT_SENTENCES) / sizeof(BUILT_SENTENCES[0]);
int index;
for (int j = 0; j < GENERATE_COUNT; j++) {
for (int i = 0; i < array_length; i++) {
updateGPS(); // Update GPS data with arbitrary values
inputHash = hash(GPS); // Get a hash of the GPS data
if (GPS.build(output, "GP", BUILT_SENTENCES[i])) {
// Randomly remove a character from the built output
int skip = random(strlen(output));
int index = 0;
for (int k = 0; k < strlen(output); k++) {
if (k != skip) {
invalid[index] = output[k];
index++;
}
}
// Remove then recalculate the checksum (this gets the sentence further
// in the parser and will cause a crash with older versions of the library)
char *p = strchr(invalid, '*');
if (p != NULL) *p = '\0';
GPS.addChecksum(invalid);
Serial.print("Invalid output: " + String(invalid));
Serial.flush();
rval = GPS.parse(invalid);
Serial.println(" - Parsed: " + String(rval?"Ok":"Error"));
} else {
Serial.println("Build Error: " + String(BUILT_SENTENCES[i]));
}
}
}
Serial.println();

#endif

Serial.println("###########################################################");
Serial.println("###########################################################");
Serial.println("###########################################################");

while(1) delay(1);
}

// Update the GPS with arbitrary data. We use this data to test the parse
// function by building NMEA sentences and then parsing them, then building them
// again using the results of the parsing and then making sure the sentences are
// the same.
void updateGPS() {
double t = millis() / 1000.;
double theta = t / 100.; // slow
double gamma = theta * 10; // faster
GPS.latitude = 4400 + sin(theta) * 60;
GPS.lat = 'N';
GPS.longitude = 7600 + cos(theta) * 60;
GPS.lon = 'W';
GPS.speed = 3 + sin(gamma);
GPS.hour = abs(cos(theta)) * 24;
GPS.minute = 30 + sin(theta / 2) * 30;
GPS.seconds = 30 + sin(gamma) * 30;
GPS.milliseconds = 500 + sin(gamma) * 500;
GPS.year = 1 + abs(sin(theta)) * 25;
GPS.month = 1 + abs(sin(gamma)) * 11;
GPS.day = 1 + abs(sin(gamma)) * 26;
GPS.satellites = abs(cos(gamma)) * 10;

GPS.geoidheight = 1 + sin(theta);
GPS.altitude = 200 + cos(gamma) * 10;
GPS.angle = abs(sin(theta)) * 360;
GPS.magvariation = abs(cos(theta)) * 360;
GPS.HDOP = 5 + sin(gamma);
GPS.VDOP = 6 + cos(gamma);
GPS.PDOP = 7 + sin(gamma);
GPS.mag = (sin(theta) < 0)?'W':'E';
GPS.fix = 1;
GPS.fixquality = 2;
GPS.fixquality_3d = 3;

GPS.depthToKeel = 2 + cos(theta);
GPS.depthToTransducer = 4 + cos(theta);
}

/*
* The FNV Hash, or more precisely the "FNV-1a alternate algorithm"
* See: http://www.isthe.com/chongo/tech/comp/fnv/
* https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function
*/

/* See the FNV parameters at www.isthe.com/chongo/tech/comp/fnv/#FNV-param */
const uint32_t FNV_32_PRIME = 0x01000193; /* 16777619 */
const uint32_t FNV_32_OFFSET = 0x811c9dc5; /* 2166136261 */

uint32_t hash(const char *str) {
size_t len = strlen(str);
unsigned char *s = (unsigned char *)str; /* unsigned string */

/* See the FNV parameters at www.isthe.com/chongo/tech/comp/fnv/#FNV-param */

uint32_t h = FNV_32_OFFSET;
while (len--) {
/* xor the bottom with the current octet */
h ^= *s++;
/* multiply by the 32 bit FNV magic prime mod 2^32 */
h *= FNV_32_PRIME;
}
return h;
}

uint32_t hash(float f) {
uint32_t ui;
memcpy( &ui, &f, sizeof( float ) );
uint32_t h = FNV_32_OFFSET;

// Note: we ignore the least significant bits of the float
h ^= (ui & 0xff000000) >> 24;
h *= FNV_32_PRIME;
h ^= (ui & 0x00ff0000) >> 16;
h *= FNV_32_PRIME;
h ^= (ui & 0x0000ff00) >> 8;
h *= FNV_32_PRIME;

return h;
}

uint32_t hash(uint8_t x) {
uint32_t h = FNV_32_OFFSET;
return h ^ x * FNV_32_PRIME;
}

uint32_t hash(char c) {
return hash((uint8_t)c);
}

uint32_t hash(bool b) {
return hash((uint8_t)b);
}

uint32_t hash(uint16_t x) {
uint32_t h = FNV_32_OFFSET;
h ^= x & 0xff;
h *= FNV_32_PRIME;
h ^= (x >> 8) & 0xff;
h *= FNV_32_PRIME;
return h;
}

uint32_t hash(int x) {
uint32_t ui = (uint32_t)x;

uint32_t h = FNV_32_OFFSET;
for (int i=0; i < sizeof(int); i++ ) {
h ^= ui & 0xff;
h *= FNV_32_PRIME;
ui = ui >> 8;
}
return h;
}

// Generate a hash of the relevant GPS information. We use this to see if the GPS
// information is identical or is substantially similar.
uint32_t hash(Adafruit_GPS gps) {
uint32_t h = 0;

h ^= hash(gps.hour);
h ^= hash(gps.minute);
h ^= hash(gps.seconds);
// Hash only the *tens* of milliseconds. Our build routine only outputs two digits
// of precision, so we won't parse the same number of milliseconds when we preparse
// our output.
h ^= hash(gps.milliseconds / 10);
h ^= hash(gps.year);
h ^= hash(gps.month);
h ^= hash(gps.day);
h ^= hash(gps.latitude);
h ^= hash(gps.longitude);
h ^= hash(gps.geoidheight);
h ^= hash(gps.altitude);
h ^= hash(gps.speed);
h ^= hash(gps.angle);
h ^= hash(gps.magvariation);
h ^= hash(gps.HDOP);
h ^= hash(gps.VDOP);
h ^= hash(gps.PDOP);
h ^= hash(gps.lat);
h ^= hash(gps.lon);
h ^= hash(gps.mag);
h ^= hash(gps.fix);
h ^= hash(gps.fixquality);
h ^= hash(gps.fixquality_3d);
h ^= hash(gps.satellites);
h ^= hash(gps.antenna);
h ^= hash(gps.depthToKeel);
h ^= hash(gps.depthToTransducer);
h ^= hash(gps.toID);
h ^= hash(gps.fromID);
h ^= hash(gps.txtTXT);
h ^= hash(gps.txtTot);
h ^= hash(gps.txtID);
h ^= hash(gps.txtN);
return h;
}