diff --git a/src/Adafruit_GPS.cpp b/src/Adafruit_GPS.cpp index 937bedb..a1e72b4 100644 --- a/src/Adafruit_GPS.cpp +++ b/src/Adafruit_GPS.cpp @@ -32,117 +32,6 @@ static bool strStartsWith(const char *str, const char *prefix); -/**************************************************************************/ -/*! - @brief Check an NMEA string for basic format, valid source ID and valid - and valid sentence ID. Update the values of thisCheck, thisSource and - thisSentence. - @param nmea Pointer to the NMEA string - @return True if well formed, false if it has problems -*/ -/**************************************************************************/ -bool Adafruit_GPS::check(char *nmea) { - thisCheck = 0; // new check - if (*nmea != '$') - return false; // doesn't start with $ - else - thisCheck += NMEA_HAS_DOLLAR; - // do checksum check -- first look if we even have one -- ignore all but last - // * - char *ast = nmea; // not strchr(nmea,'*'); for first * - while (*ast) - ast++; // go to the end - while (*ast != '*' && ast > nmea) - ast--; // then back to * if it's there - if (*ast != '*') - return false; // there is no asterisk - else { - uint16_t sum = parseHex(*(ast + 1)) * 16; // extract checksum - sum += parseHex(*(ast + 2)); - char *p = nmea; // check checksum - for (char *p1 = p + 1; p1 < ast; p1++) - sum ^= *p1; - if (sum != 0) - return false; // bad checksum :( - else - thisCheck += NMEA_HAS_CHECKSUM; - } - // extract source of variable length - char *p = nmea + 1; - const char *src = tokenOnList(p, sources); - if (src) { - strcpy(thisSource, src); - thisCheck += NMEA_HAS_SOURCE; - } else - return false; - p += strlen(src); - // extract sentence id and check if parsed - const char *snc = tokenOnList(p, sentences_parsed); - if (snc) { - strcpy(thisSentence, snc); - thisCheck += NMEA_HAS_SENTENCE_P + NMEA_HAS_SENTENCE; - } else { // check if known - snc = tokenOnList(p, sentences_known); - if (snc) { - strcpy(thisSentence, snc); - thisCheck += NMEA_HAS_SENTENCE; - return false; - } - } - return true; // passed all the tests -} - -/**************************************************************************/ -/*! - @brief Check if a token at the start of a string is on a list. - @param token Pointer to the string - @param list A list of strings, with the final entry starting "ZZ" - @return Pointer to the found token, or NULL if it fails -*/ -/**************************************************************************/ -const char *Adafruit_GPS::tokenOnList(char *token, const char **list) { - int i = 0; // index in the list - while (strncmp(list[i], "ZZ", 2) && - i < 1000) { // stop at terminator and don't crash without it - // test for a match on the sentence name - if (!strncmp((const char *)list[i], (const char *)token, strlen(list[i]))) - return list[i]; - i++; - } - return NULL; // couldn't find a match -} - -/**************************************************************************/ -/*! - @brief Parse a string token from pointer p to the next comma, asterisk - or end of string. - @param buff Pointer to the buffer to store the string in - @param p Pointer into a string - @param n Max permitted size of string including terminating 0 - @return Pointer to the string buffer -*/ -/**************************************************************************/ -char *Adafruit_GPS::parseStr(char *buff, char *p, int n) { - char *e = strchr(p, ','); - int len = 0; - if (e) { - len = min(e - p, n - 1); - strncpy(buff, p, len); // copy up to the comma - buff[len] = 0; - } else { - e = strchr(p, '*'); - if (e) { - len = min(e - p, n - 1); - strncpy(buff, p, len); // or up to the * - buff[e - p] = 0; - } else { - len = min((int)strlen(p), n - 1); - strncpy(buff, p, len); // or to the end or max capacity - } - } - return buff; -} - /**************************************************************************/ /*! @brief Is the field empty, or should we try conversion? Won't work @@ -151,32 +40,6 @@ char *Adafruit_GPS::parseStr(char *buff, char *p, int n) { @param pStart Pointer to the location of the token in the NMEA string @return true if empty field, false if something there */ -/**************************************************************************/ -bool Adafruit_GPS::isEmpty(char *pStart) { - if (',' != *pStart && '*' != *pStart && pStart != NULL) - return false; - else - return true; -} - -/**************************************************************************/ -/*! - @brief Parse a part of an NMEA string for time - @param p Pointer to the location of the token in the NMEA string -*/ -/**************************************************************************/ -void Adafruit_GPS::parseTime(char *p) { - // get time - uint32_t time = atol(p); - hour = time / 10000; - minute = (time % 10000) / 100; - seconds = (time % 100); - - p = strchr(p, '.') + 1; - milliseconds = atoi(p); - lastTime = sentTime; -} - /**************************************************************************/ /*! @brief Parse a part of an NMEA string for latitude angle @@ -275,24 +138,6 @@ bool Adafruit_GPS::parseLonDir(char *p) { return true; } -/**************************************************************************/ -/*! - @brief Parse a part of an NMEA string for whether there is a fix - @param p Pointer to the location of the token in the NMEA string - @return True if we parsed it, false if it has invalid data -*/ -/**************************************************************************/ -bool Adafruit_GPS::parseFix(char *p) { - if (p[0] == 'A') { - fix = true; - lastFix = sentTime; - } else if (p[0] == 'V') - fix = false; - else - return false; - return true; -} - /**************************************************************************/ /*! @brief Time in seconds since the last position fix was obtained. Will @@ -682,28 +527,6 @@ char *Adafruit_GPS::lastNMEA(void) { return (char *)lastline; } -/**************************************************************************/ -/*! - @brief Parse a hex character and return the appropriate decimal value - @param c Hex character, e.g. '0' or 'B' - @return Integer value of the hex character. Returns 0 if c is not a proper - character -*/ -/**************************************************************************/ -// read a Hex value and return the decimal equivalent -uint8_t Adafruit_GPS::parseHex(char c) { - if (c < '0') - return 0; - if (c <= '9') - return c - '0'; - if (c < 'A') - return 0; - if (c <= 'F') - return (c - 'A') + 10; - // if (c > 'F') - return 0; -} - /**************************************************************************/ /*! @brief Wait for a specified sentence from the device diff --git a/src/Adafruit_GPS.h b/src/Adafruit_GPS.h index 75cf7b3..601ca72 100644 --- a/src/Adafruit_GPS.h +++ b/src/Adafruit_GPS.h @@ -100,14 +100,10 @@ public: void pause(bool b); - uint8_t parseHex(char c); - char read(void); size_t write(uint8_t); size_t available(void); - bool check(char *nmea); - bool parse(char *); nmea_float_t secondsSinceFix(); nmea_float_t secondsSinceTime(); nmea_float_t secondsSinceDate(); @@ -116,6 +112,12 @@ public: bool wakeup(void); bool standby(void); + // NMEA_parse.cpp + bool parse(char *); + bool check(char *nmea); + bool onList(char *nmea, const char **list); + uint8_t parseHex(char c); + // NMEA_build.cpp #ifdef NMEA_EXTENSIONS char *build(char *nmea, const char *thisSource, const char *thisSentence, @@ -234,17 +236,22 @@ public: #endif // NMEA_EXTENSIONS private: - const char *tokenOnList(char *token, const char **list); - char *parseStr(char *buff, char *p, int n); - bool isEmpty(char *pStart); - void parseTime(char *); void parseLat(char *); bool parseLatDir(char *); void parseLon(char *); bool parseLonDir(char *); // NMEA_data.cpp void data_init(); + // NMEA_parse.cpp + const char *tokenOnList(char *token, const char **list); + bool parseCoord(char *p, nmea_float_t *angleDegrees = NULL, + nmea_float_t *angle = NULL, int32_t *angle_fixed = NULL, + char *dir = NULL); + char *parseStr(char *buff, char *p, int n); + bool parseTime(char *); bool parseFix(char *); + bool isEmpty(char *pStart); + // used by check() for validity tests, room for future expansion const char *sources[6] = {"II", "WI", "GP", "GN", "P", "ZZZ"}; ///< valid source ids diff --git a/src/NMEA_parse.cpp b/src/NMEA_parse.cpp index ed3a0bb..0b98b9d 100644 --- a/src/NMEA_parse.cpp +++ b/src/NMEA_parse.cpp @@ -32,112 +32,102 @@ /**************************************************************************/ /*! - @brief Parse a NMEA string + @brief Parse a standard NMEA string and update the relevant variables. + Sentences start with a $, then a two character source identifier, then a + three character sentence identifier that defines the format, then a comma and + more comma separated fields defined by the sentence name. There are many + sentences listed that are not yet supported, including proprietary sentences + that start with P, like the $PMTK commands to the GPS modules. See the + build() function and http://fort21.ru/download/NMEAdescription.pdf for + sentence descriptions. + + Encapsulated data sentences are supported by NMEA-183, and start with ! + instead of $. https://gpsd.gitlab.io/gpsd/AIVDM.html provides details + about encapsulated data sentences used in AIS. + + parse() permits, but does not require Carriage Return and Line Feed at the + end of sentences. The end of the sentence is recognized by the * for the + checksum. parse() will not recognize a sentence without a valid checksum. + + NMEA_EXTENSIONS must be defined in order to parse more than basic + GPS module sentences. + @param nmea Pointer to the NMEA string - @return True if we parsed it, false if it has an invalid checksum or invalid - data + @return True if successfully parsed, false if fails check or parsing */ /**************************************************************************/ bool Adafruit_GPS::parse(char *nmea) { - // do checksum check if (!check(nmea)) return false; // passed the check, so there's a valid source in thisSource and a valid // sentence in thisSentence - - // look for a few common sentences char *p = nmea; // Pointer to move through the sentence -- good parsers are // non-destructive - p = strchr(p, ',') + - 1; // Skip to the character after the next comma, then check sentence. + p = strchr(p, ',') + 1; // Skip to char after the next comma, then check. - if (!strcmp(thisSentence, "GGA")) { - // found GGA - // get time + // 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 + // Adafruit from Actisense NGW-1 from SH CP150C parseTime(p); - - // parse out latitude + 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; - parseLat(p); p = strchr(p, ',') + 1; - if (!parseLatDir(p)) - return false; - - // parse out longitude + // 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; - parseLon(p); p = strchr(p, ',') + 1; - if (!parseLonDir(p)) - return false; - - p = strchr(p, ',') + 1; - if (!isEmpty(p)) { - fixquality = atoi(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 if (fixquality > 0) { fix = true; lastFix = sentTime; } else fix = false; } - - p = strchr(p, ',') + 1; - if (!isEmpty(p)) { + p = strchr(p, ',') + 1; // then move on to the next + // Most can just be parsed with atoi() or atof(), then move on to the next. + if (!isEmpty(p)) satellites = atoi(p); - } - p = strchr(p, ',') + 1; - if (!isEmpty(p)) { - HDOP = atof(p); - } - + if (!isEmpty(p)) + newDataValue(NMEA_HDOP, HDOP = atof(p)); p = strchr(p, ',') + 1; - if (!isEmpty(p)) { + if (!isEmpty(p)) altitude = atof(p); - } - p = strchr(p, ',') + 1; - p = strchr(p, ',') + 1; - if (!isEmpty(p)) { - geoidheight = atof(p); - } - } + p = strchr(p, ',') + 1; // skip the units + if (!isEmpty(p)) + geoidheight = atof(p); // skip the rest - else if (!strcmp(thisSentence, "RMC")) { - // found RMC - // get time + } else if (!strcmp(thisSentence, "RMC")) { //*****************************RMC + // in Adafruit from Actisense NGW-1 from SH CP150C parseTime(p); - - // fix or no fix p = strchr(p, ',') + 1; - if (!parseFix(p)) - return false; - - // parse out latitude + parseFix(p); p = strchr(p, ',') + 1; - parseLat(p); + // 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; - if (!parseLatDir(p)) - return false; - - // parse out longitude p = strchr(p, ',') + 1; - parseLon(p); + // 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; - if (!parseLonDir(p)) - return false; - - // speed p = strchr(p, ',') + 1; - if (!isEmpty(p)) { - speed = atof(p); - } - - // angle + if (!isEmpty(p)) + newDataValue(NMEA_SOG, speed = atof(p)); p = strchr(p, ',') + 1; - if (!isEmpty(p)) { - angle = atof(p); - } - + if (!isEmpty(p)) + newDataValue(NMEA_COG, angle = atof(p)); p = strchr(p, ',') + 1; if (!isEmpty(p)) { uint32_t fulldate = atof(p); @@ -145,66 +135,264 @@ bool Adafruit_GPS::parse(char *nmea) { month = (fulldate % 10000) / 100; year = (fulldate % 100); lastDate = sentTime; - } - } + } // skip the rest - else if (!strcmp(thisSentence, "GLL")) { - // found GLL - // parse out latitude - parseLat(p); + } else if (!strcmp(thisSentence, "GLL")) { //*****************************GLL + // 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; - if (!parseLatDir(p)) - return false; - - // parse out longitude p = strchr(p, ',') + 1; - parseLon(p); + // 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; - if (!parseLonDir(p)) - return false; - - // get time p = strchr(p, ',') + 1; parseTime(p); - - // fix or no fix p = strchr(p, ',') + 1; - if (!parseFix(p)) - return false; - } + parseFix(p); // skip the rest - else if (!strcmp(thisSentence, "GSA")) { - // found GSA - // parse out Auto selection, but ignore them - // parse out 3d fixquality - p = strchr(p, ',') + 1; - if (!isEmpty(p)) { + } else if (!strcmp(thisSentence, "GSA")) { //*****************************GSA + // in Adafruit from Actisense NGW-1 + p = strchr(p, ',') + 1; // skip selection mode + if (!isEmpty(p)) fixquality_3d = atoi(p); - } + p = strchr(p, ',') + 1; // skip 12 Satellite PDNs without interpreting them for (int i = 0; i < 12; i++) p = strchr(p, ',') + 1; - - // parse out PDOP - p = strchr(p, ',') + 1; - if (!isEmpty(p)) { + if (!isEmpty(p)) PDOP = atof(p); - } + p = strchr(p, ',') + 1; // parse out HDOP, we also parse this from the GGA sentence. Chipset should // report the same for both + if (!isEmpty(p)) + newDataValue(NMEA_HDOP, HDOP = atof(p)); p = strchr(p, ',') + 1; - if (!isEmpty(p)) { - HDOP = atof(p); - } - // parse out VDOP - p = strchr(p, ',') + 1; - if (!isEmpty(p)) { - VDOP = atof(p); - } - } + if (!isEmpty(p)) + VDOP = atof(p); // last before checksum + } #ifdef NMEA_EXTENSIONS // Sentences not required for basic GPS functionality - else if (!strcmp(thisSentence, "TXT")) { //*******************************TXT + else if (!strcmp(thisSentence, "APB")) { //*******************************APB + // from Actisense NGW-1 from SH CP150C + return false; + + } else if (!strcmp(thisSentence, "DBT")) { //*****************************DBT + // from Actisense NGW-1 + // feet, metres, fathoms below transducer coerced to water depth from + // surface in metres + if (!isEmpty(p)) + newDataValue(NMEA_DEPTH, atof(p) * 0.3048 + depthToTransducer); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_DEPTH, atof(p) + depthToTransducer); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_DEPTH, atof(p) * 6 * 0.3048 + depthToTransducer); + + } else if (!strcmp(thisSentence, "DPT")) { //*****************************DPT + // from Actisense NGW-1 + return false; + + } else if (!strcmp(thisSentence, "GSV")) { //*****************************GSV + // from Actisense NGW-1 + return false; + + } else if (!strcmp(thisSentence, "HDG")) { //*****************************HDG + // from Actisense NGW-1 from SH CP150C + return false; + + } else if (!strcmp(thisSentence, "HDM")) { //*****************************HDM + if (!isEmpty(p)) + newDataValue(NMEA_HDG, atof(p)); // skip the rest + + } else if (!strcmp(thisSentence, "HDT")) { //*****************************HDT + if (!isEmpty(p)) + newDataValue(NMEA_HDT, atof(p)); // skip the rest + + } else if (!strcmp(thisSentence, "MDA")) { //*****************************MDA + // from Actisense NGW-1 + if (!isEmpty(p)) + newDataValue(NMEA_BAROMETER, atof(p) * 3386.39); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_BAROMETER, atof(p) * 100000); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + nmea_float_t T = 100000.; + char u = 'C'; + if (!isEmpty(p)) + T = atof(p); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + u = *p; + p = strchr(p, ',') + 1; + if (u != 'C') { + T = (T - 32) / 1.8; + u = 'C'; + } // coerce to C + if (T < 1000) + newDataValue(NMEA_TEMPERATURE_AIR, T); + T = 100000.; + u = 'C'; + if (!isEmpty(p)) + T = atof(p); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + u = *p; + p = strchr(p, ',') + 1; + if (u != 'C') { + T = (T - 32) / 1.8; + u = 'C'; + } + if (T < 1000) + newDataValue(NMEA_TEMPERATURE_WATER, T); + if (!isEmpty(p)) + newDataValue(NMEA_HUMIDITY, atof(p)); // skip the rest + + } else if (!strcmp(thisSentence, "MTW")) { //*****************************MTW + nmea_float_t T = 100000.; + char u = 'C'; + if (!isEmpty(p)) + T = atof(p); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + u = *p; // last before checksum + if (u != 'C') { + T = (T - 32) / 1.8; + u = 'C'; + } + if (T < 1000) + newDataValue(NMEA_TEMPERATURE_WATER, T); + + } else if (!strcmp(thisSentence, "MWD")) { //*****************************MWD + // from Actisense NGW-1 + return false; + + } else if (!strcmp(thisSentence, "MWV")) { //*****************************MWV + // from Actisense NGW-1 + nmea_float_t ang = 100000.; + char ref = 'T'; + if (!isEmpty(p)) + ang = atof(p); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + ref = *p; + p = strchr(p, ',') + 1; + nmea_float_t spd = 100000.; + if (!isEmpty(p)) + spd = atof(p); + p = strchr(p, ',') + 1; + char units = 'N'; + if (!isEmpty(p)) + units = *p; + p = strchr(p, ',') + 1; + char stat = 'A'; + if (!isEmpty(p)) + stat = *p; // last before checksum + if (units == 'K') { + spd /= 1.6; + units = 'M'; + } + if (units == 'M') { + spd *= 5280. / 6000.; + units = 'N'; + } + if (ang > 180.) + ang -= 360.; + if (ref == 'R') { + if (ang < 1000. && stat == 'A') + newDataValue(NMEA_AWA, ang); + if (spd < 1000. && stat == 'A') + newDataValue(NMEA_AWS, spd); + } else { + if (ang < 1000. && stat == 'A') + newDataValue(NMEA_TWA, ang); + if (spd < 1000. && stat == 'A') + newDataValue(NMEA_TWS, spd); + } + + } else if (!strcmp(thisSentence, "RMB")) { //*****************************RMB + // from Actisense NGW-1 from SH CP150C + p = strchr(p, ',') + 1; // skip status + nmea_float_t xte = 100000.; + char xteDir = 'X'; + if (!isEmpty(p)) + xte = atof(p); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + xteDir = *p; + p = strchr(p, ',') + 1; + if (xte < 10000. && xteDir != 'X') { + if (xteDir == 'L') + xte *= -1.; + newDataValue(NMEA_XTE, xte); + } + if (!isEmpty(p)) + parseStr(toID, p, NMEA_MAX_WP_ID); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + parseStr(fromID, p, NMEA_MAX_WP_ID); + p = strchr(p, ',') + 1; + nmea_float_t latitudeWP = + 0; // All the same position data for the next way point + nmea_float_t longitudeWP = 0; + int32_t latitude_fixedWP = 0; + int32_t longitude_fixedWP = 0; + nmea_float_t latitudeDegreesWP = 0; + nmea_float_t longitudeDegreesWP = 0; + char latWP = 'N'; + char lonWP = 'W'; + + // parse out both latitude and direction for WayPoint, then go to next + // field, or fail + if (!isEmpty(p)) { + if (!parseCoord(p, &latitudeDegreesWP, &latitudeWP, &latitude_fixedWP, + &latWP)) + return false; + else + newDataValue(NMEA_LATWP, latitudeDegreesWP); + } + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + // parse out both longitude and direction for WayPoint, then go to next + // field, or fail + if (!isEmpty(p)) { + if (!parseCoord(p, &longitudeDegreesWP, &longitudeWP, &longitude_fixedWP, + &lonWP)) + return false; + else + newDataValue(NMEA_LONWP, longitudeDegreesWP); + } + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_DISTWP, atof(p)); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_COGWP, atof(p)); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_VMGWP, atof(p)); // skip arrival flag + + } else if (!strcmp(thisSentence, "ROT")) { //*****************************ROT + return false; + + } else if (!strcmp(thisSentence, "RPM")) { //*****************************RPM + return false; + + } else if (!strcmp(thisSentence, "RSA")) { //*****************************RSA + // from Actisense NGW-1 + return false; + + } else if (!strcmp(thisSentence, "TXT")) { //*****************************TXT if (!isEmpty(p)) txtTot = atoi(p); p = strchr(p, ',') + 1; @@ -216,12 +404,128 @@ bool Adafruit_GPS::parse(char *nmea) { p = strchr(p, ',') + 1; if (!isEmpty(p)) parseStr(txtTXT, p, 61); // copy the text to NMEA TXT max of 61 characters + + } else if (!strcmp(thisSentence, "VDR")) { //*****************************VDR + // from Actisense NGW-1 + return false; + + } else if (!strcmp(thisSentence, "VHW")) { //*****************************VHW + // from Actisense NGW-1 + if (!isEmpty(p)) + newDataValue(NMEA_HDT, atof(p)); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_HDG, atof(p)); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_VTW, atof(p)); // skip the other units + + } else if (!strcmp(thisSentence, "VLW")) { //*****************************VLW + // from Actisense NGW-1 + if (!isEmpty(p)) + newDataValue(NMEA_LOG, atof(p)); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + newDataValue(NMEA_LOGR, atof(p)); // skip the other units + + } else if (!strcmp(thisSentence, "VPW")) { //*****************************VPW + // knots, metres/s coerced to knots + nmea_float_t vmg = 100000.; + if (!isEmpty(p)) + vmg = atof(p); + p = strchr(p, ',') + 1; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + vmg = atof(p) * 0.3048 * 3600. / 6000.; // skip units + if (vmg < 1000.) + newDataValue(NMEA_VMG, vmg); + } else if (!strcmp(thisSentence, "VTG")) { //*****************************VTG + // from Actisense NGW-1 from SH CP150C + return false; + + } else if (!strcmp(thisSentence, "VWR")) { //*****************************VWR + // from Actisense NGW-1 + nmea_float_t ang = 1000.; + if (!isEmpty(p)) + ang = atof(p); + p = strchr(p, ',') + 1; + char ref = ' '; + if (!isEmpty(p)) + ref = *p; + p = strchr(p, ',') + 1; + if (ref == 'L') + ang *= -1; + if (ang < 1000.) + newDataValue(NMEA_AWA, ang); + nmea_float_t ws = 0.0; + char units = 'X'; + if (!isEmpty(p)) + ws = atof(p); + p = strchr(p, ',') + 1; // knots + if (!isEmpty(p)) + units = *p; + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + ws = atof(p); + p = strchr(p, ',') + 1; // meters / second + if (!isEmpty(p)) + units = *p; + p = strchr(p, ',') + 1; // M + if (!isEmpty(p)) + ws = atof(p); + p = strchr(p, ',') + 1; // kilometers / hour can be converted back to knots + if (!isEmpty(p)) + units = *p; // last before checksum + if (units == 'M') { + ws *= 3.6; + units = 'K'; + } // convert m/s to km/h + if (units == 'K') { + ws /= 1.6; + units = 'M'; + } // convert km/h to miles / h + if (units == 'M') { + ws *= 5280. / 6000.; + units = 'N'; + } // convert miles / hr to knots + if (units == 'N') + newDataValue(NMEA_AWS, ws); // store the final result + + } else if (!strcmp(thisSentence, "WCV")) { //*****************************WCV + // from SH CP150C + if (!isEmpty(p)) + newDataValue(NMEA_VMGWP, atof(p)); // skip the rest + + } else if (!strcmp(thisSentence, "XTE")) { //*****************************XTE + // from Actisense NGW-1 from SH CP150C + p = strchr(p, ',') + 1; // skip status 1 + p = strchr(p, ',') + 1; // skip status 2 + nmea_float_t xte = 100000.; + char xteDir = 'X'; + if (!isEmpty(p)) + xte = atof(p); + p = strchr(p, ',') + 1; + if (!isEmpty(p)) + xteDir = *p; + p = strchr(p, ',') + 1; + if (xte < 10000. && xteDir != 'X') { + if (xteDir == 'L') + xte *= -1.; + newDataValue(NMEA_XTE, xte); + } // skip units + + } else if (!strcmp(thisSentence, "ZDA")) { //*****************************ZDA + // from Actisense NGW-1 + return false; } #endif // NMEA_EXTENSIONS - // we dont parse the remaining, yet! - else - return false; + else { + return false; // didn't find the required sentence definition + } // Record the successful parsing of where the last data came from and when strcpy(lastSource, thisSource); @@ -229,3 +533,298 @@ bool Adafruit_GPS::parse(char *nmea) { lastUpdate = millis(); return true; } + +/**************************************************************************/ +/*! + @brief Check an NMEA string for basic format, valid source ID and valid + and valid sentence ID. Update the values of thisCheck, thisSource and + thisSentence. + @param nmea Pointer to the NMEA string + @return True if well formed, false if it has problems +*/ +/**************************************************************************/ +boolean Adafruit_GPS::check(char *nmea) { + thisCheck = 0; // new check + if (*nmea != '$' && *nmea != '!') + return false; // doesn't start with $ or ! + else + thisCheck += NMEA_HAS_DOLLAR; + // do checksum check -- first look if we even have one -- ignore all but last + // * + char *ast = nmea; // not strchr(nmea,'*'); for first * + while (*ast) + ast++; // go to the end + while (*ast != '*' && ast > nmea) + ast--; // then back to * if it's there + if (*ast != '*') + return false; // there is no asterisk + else { + uint16_t sum = parseHex(*(ast + 1)) * 16; // extract checksum + sum += parseHex(*(ast + 2)); + char *p = nmea; // check checksum + for (char *p1 = p + 1; p1 < ast; p1++) + sum ^= *p1; + if (sum != 0) + return false; // bad checksum :( + else + thisCheck += NMEA_HAS_CHECKSUM; + } + // extract source of variable length + char *p = nmea + 1; + const char *src = tokenOnList(p, sources); + if (src) { + strcpy(thisSource, src); + thisCheck += NMEA_HAS_SOURCE; + } else + return false; + p += strlen(src); + // extract sentence id and check if parsed + const char *snc = tokenOnList(p, sentences_parsed); + if (snc) { + strcpy(thisSentence, snc); + thisCheck += NMEA_HAS_SENTENCE_P + NMEA_HAS_SENTENCE; + } else { // check if known + snc = tokenOnList(p, sentences_known); + if (snc) { + strcpy(thisSentence, snc); + thisCheck += NMEA_HAS_SENTENCE; + return false; + } + } + return true; // passed all the tests +} + +/**************************************************************************/ +/*! + @brief Check if a token at the start of a string is on a list. + @param token Pointer to the string + @param list A list of strings, with the final entry starting "ZZ" + @return Pointer to the found token, or NULL if it fails +*/ +/**************************************************************************/ +const char *Adafruit_GPS::tokenOnList(char *token, const char **list) { + int i = 0; // index in the list + while (strncmp(list[i], "ZZ", 2) && + i < 1000) { // stop at terminator and don't crash without it + // test for a match on the sentence name + if (!strncmp((const char *)list[i], (const char *)token, strlen(list[i]))) + return list[i]; + i++; + } + return NULL; // couldn't find a match +} + +/**************************************************************************/ +/*! + @brief Check if an NMEA string is valid and is on a list, perhaps to + decide if it should be passed to a particular NMEA device. + @param nmea Pointer to the NMEA string + @param list A list of strings, with the final entry "ZZ" + @return True if on the list, false if it fails check or is not on the list +*/ +/**************************************************************************/ +bool Adafruit_GPS::onList(char *nmea, const char **list) { + if (!check(nmea)) // sets thisSentence if valid + return false; // not a valid sentence + // stop at terminator with first two letters ZZ and don't crash without it + for (int i = 0; strncmp(list[i], "ZZ", 2) && i < 1000; i++) { + // test for a match on the sentence name + if (!strcmp((const char *)list[i], (const char *)thisSentence)) + return true; + } + return false; // couldn't find a match +} + +/**************************************************************************/ +/*! + @brief Parse a part of an NMEA string for lat or lon angle and direction. + Works for either DDMM.mmmm,N (latitude) or DDDMM.mmmm,W (longitude) format. + Insensitive to number of decimal places present. Only fills the variables + if it succeeds and the variable pointer is not NULL. This allows calling + to fill only the variables of interest. Does rudimentary validation on + angle range. + + Supersedes private functions parseLat(), parseLon(), parseLatDir(), + parseLonDir(), all previously called from parse(). + @param pStart Pointer to the location of the token in the NMEA string + @param angle Pointer to the angle to fill with value in degrees/minutes as + received from the GPS (DDDMM.MMMM), unsigned + @param angle_fixed Pointer to the fix point version latitude in decimal + degrees * 10000000, signed + @param angleDegrees Pointer to the angle to fill with decimal degrees, + signed. As actual double on SAMD, etc. resolution is better than the + fixed point version. + @param dir Pointer to character to fill the direction N/S/E/W + @return true if successful, false if failed or no value +*/ +/**************************************************************************/ +bool Adafruit_GPS::parseCoord(char *pStart, nmea_float_t *angleDegrees, + nmea_float_t *angle, int32_t *angle_fixed, + char *dir) { + char *p = pStart; + if (!isEmpty(p)) { + // get the number in DDDMM.mmmm format and break into components + char degreebuff[10]; + char *e = strchr(p, '.'); + if (e == NULL || e - p > 6) + return false; // no decimal point in range + strncpy(degreebuff, p, e - p); // get DDDMM + long dddmm = atol(degreebuff); + long degrees = (dddmm / 100); // truncate the minutes + long minutes = dddmm - degrees * 100; // remove the degrees + p = e; // start from the decimal point + nmea_float_t decminutes = atof(e); // the fraction after the decimal point + p = strchr(p, ',') + 1; // go to the next field + + // get the NSEW direction as a character + char nsew = 'X'; + if (!isEmpty(p)) + nsew = *p; // field is not empty + else + return false; // no direction provided + + // set the various numerical formats to their values + long fixed = degrees * 10000000 + (minutes * 10000000) / 60 + + (decminutes * 10000000) / 60; + nmea_float_t ang = degrees * 100 + minutes + decminutes; + nmea_float_t deg = fixed / (nmea_float_t)10000000.; + if (nsew == 'S' || + nsew == 'W') { // fixed and deg are signed, but DDDMM.mmmm is not + fixed = -fixed; + deg = -deg; + } + + // reject directions that are not NSEW + if (nsew != 'N' && nsew != 'S' && nsew != 'E' && nsew != 'W') + return false; + + // reject angles that are out of range + if (nsew == 'N' || nsew == 'S') + if(abs(deg) > 90) return false; + if(abs(deg) > 180) return false; + + // store in locations passed as args + if (angle != NULL) + *angle = ang; + if (angle_fixed != NULL) + *angle_fixed = fixed; + if (angleDegrees != NULL) + *angleDegrees = deg; + if (dir != NULL) + *dir = nsew; + } else + return false; // no number + return true; +} + +/**************************************************************************/ +/*! + @brief Parse a string token from pointer p to the next comma, asterisk + or end of string. + @param buff Pointer to the buffer to store the string in + @param p Pointer into a string + @param n Max permitted size of string including terminating 0 + @return Pointer to the string buffer +*/ +/**************************************************************************/ +char *Adafruit_GPS::parseStr(char *buff, char *p, int n) { + char *e = strchr(p, ','); + int len = 0; + if (e) { + len = min(e - p, n - 1); + strncpy(buff, p, len); // copy up to the comma + buff[len] = 0; + } else { + e = strchr(p, '*'); + if (e) { + len = min(e - p, n - 1); + strncpy(buff, p, len); // or up to the * + buff[e - p] = 0; + } else { + len = min((int)strlen(p), n - 1); + strncpy(buff, p, len); // or to the end or max capacity + } + } + return buff; +} + +/**************************************************************************/ +/*! + @brief Parse a part of an NMEA string for time. Independent of number + of decimal places after the '.' + @param p Pointer to the location of the token in the NMEA string + @return true if successful, false otherwise +*/ +/**************************************************************************/ +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); + p = strchr(p, '.'); + milliseconds = atof(p) * 1000; + lastTime = sentTime; + return true; + } + return false; +} + +/**************************************************************************/ +/*! + @brief Parse a part of an NMEA string for whether there is a fix + @param p Pointer to the location of the token in the NMEA string + @return True if we parsed it, false if it has invalid data +*/ +/**************************************************************************/ +boolean Adafruit_GPS::parseFix(char *p) { + if (!isEmpty(p)) { + if (p[0] == 'A') { + fix = true; + lastFix = sentTime; + } else if (p[0] == 'V') + fix = false; + else + return false; + return true; + } + return false; +} + +/**************************************************************************/ +/*! + @brief Is the field empty, or should we try conversion? Won't work + for a text field that starts with an asterisk or a comma, but that + probably violates the NMEA-183 standard. + @param pStart Pointer to the location of the token in the NMEA string + @return true if empty field, false if something there +*/ +/**************************************************************************/ +bool Adafruit_GPS::isEmpty(char *pStart) { + if (',' != *pStart && '*' != *pStart && pStart != NULL) + return false; + else + return true; +} + +/**************************************************************************/ +/*! + @brief Parse a hex character and return the appropriate decimal value + @param c Hex character, e.g. '0' or 'B' + @return Integer value of the hex character. Returns 0 if c is not a proper + character +*/ +/**************************************************************************/ +// read a Hex value and return the decimal equivalent +uint8_t Adafruit_GPS::parseHex(char c) { + if (c < '0') + return 0; + if (c <= '9') + return c - '0'; + if (c < 'A') + return 0; + if (c <= 'F') + return (c - 'A') + 10; + // if (c > 'F') + return 0; +}