diff --git a/examples/GPS_HardwareSerial_Timing/GPS_HardwareSerial_Timing.ino b/examples/GPS_HardwareSerial_Timing/GPS_HardwareSerial_Timing.ino index 41ab240..de57086 100644 --- a/examples/GPS_HardwareSerial_Timing/GPS_HardwareSerial_Timing.ino +++ b/examples/GPS_HardwareSerial_Timing/GPS_HardwareSerial_Timing.ino @@ -34,9 +34,10 @@ Adafruit_GPS GPS(&GPSSerial); #ifdef NMEA_EXTENSIONS // Create another GPS object to hold the state of the boat, with no -// communications, so don't call Boat.begin() in setup. We will build some fake -// sentences from the Boat data to feed to GPS for testing. -Adafruit_GPS Boat(&GPSSerial); +// communications, so you don't need to call Boat.begin() in setup. +// We will build some fake sentences from the Boat data to feed to +// GPS for testing. +Adafruit_GPS Boat; #endif // Set GPSECHO to 'false' to turn off echoing the GPS data to the Serial console diff --git a/examples/NMEA_EXTENSIONS/NMEA_EXTENSIONS.ino b/examples/NMEA_EXTENSIONS/NMEA_EXTENSIONS.ino new file mode 100644 index 0000000..047bc11 --- /dev/null +++ b/examples/NMEA_EXTENSIONS/NMEA_EXTENSIONS.ino @@ -0,0 +1,143 @@ +/**************************************************************************/ +/*! + @file NMEA_EXTENSIONS.ino + + @section intro Introduction + + An Arduino sketch for testing the NMEA_EXTENSIONS to the library. Does + not require any GPS hardware. Boat is a data only object we can use to + represent the actual data and build sentences from. GPS is a data only + object that parses the sentences and saves the results, the same way you + would with a communicating GPS object + + Only some of the data values will have added history. Note that history + is stored as integers, scaled and offset from the float values to save + memory. The AWA (Apparent Wind Angle) is recorded as three components, + so that sin and cos parts can be accurately time averaged. onList() allows + testing sentences against a list to see if they should be passed on to + another listener, allowing your sketch to act as an NMEA multiplexer. + + Although it will just barely compile for an UNO with the NMEA_EXTENSIONS, + defining two GPS objects pushes the limits of the UNO data space and + should probably be avoided. + + @section author Author + + Written by Rick Sellens. + + @section license License + + CCBY license +*/ +/**************************************************************************/ +#include "Adafruit_GPS.h" +Adafruit_GPS GPS; // The results obtained from the instruments -- no comms +Adafruit_GPS Boat; // The state of the boat used to create some simulated sentences + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 10000); // Wait for monitor to be ready. + Serial.print("\n\nNMEA_EXTENSIONS Example v 0.1.1\n\n"); + #ifdef NMEA_EXTENSIONS + addHistory(&GPS); + #else + Serial.print("NMEA_EXTENSIONS not #defined, so there will be no action.\n"); + #endif +} + +char latestBoat[200] = ""; + +const char *senList[] = {"GGA", "GLL", "DBT", "HDM", "MWV", "ZZ"}; // sentence list +const char *passList[] = {"GGA", "DBT", "ZZ"}; // short list + +void loop() { + static unsigned long lastPrint = 0; + updateBoat(); + // stop keeping AWA history after 30 seconds, just as a demonstration + #ifdef NMEA_EXTENSIONS + if(millis() > 30000) GPS.removeHistory(NMEA_AWA); + #endif + if (millis() - lastPrint > 300 || lastPrint == 0) { + lastPrint = millis(); +#ifdef NMEA_EXTENSIONS + Serial.print("\nSentences built from Boat and parsed by GPS "); + Serial.print("(Only a few get passed on to the ST network.):\n\n"); + for (int i = 0;strncmp(senList[i],"ZZ",2);i++){ + if (GPS.parse(Boat.build(latestBoat, "II", senList[i]))){ + if (GPS.onList(latestBoat,passList)) + Serial.print("Pass to ST: "); + else Serial.print(" No Pass: "); + Serial.print(latestBoat); + } else { + Serial.print("Couldn't build and parse a "); + Serial.print(senList[i]); + Serial.print(" sentence, maybe because sprintf() doesn't work with %f."); + } + } + + Serial.print("\nSome of the resulting data stored in GPS:\n\n"); + GPS.showDataValue(NMEA_LAT); + GPS.showDataValue(NMEA_LON); + GPS.showDataValue(NMEA_AWA, 20); // show more history values, if history on + GPS.showDataValue(NMEA_AWA_SIN); + GPS.showDataValue(NMEA_AWA_COS); + GPS.showDataValue(NMEA_AWS); + GPS.showDataValue(NMEA_HDG); + GPS.showDataValue(NMEA_DEPTH); + + Serial.print("\nThe AWA is: "); + Serial.print(GPS.get(NMEA_AWA)); + Serial.print(" while the smoothed value is: "); + Serial.println(GPS.getSmoothed(NMEA_AWA)); + +#endif // NMEA_Extensions + } +} + +void updateBoat() { // Fill up the boat values with + // some test data to use in build() + nmea_float_t t = millis() / 1000.; + nmea_float_t theta = t / 100.; // slow + nmea_float_t gamma = theta * 10; // faster + + // add some data to the old Adafruit_GPS variables + Boat.latitude = 4400 + sin(theta) * 60; + Boat.lat = 'N'; + Boat.longitude = 7600 + cos(theta) * 60; + Boat.lon = 'W'; + Boat.fixquality = 2; + Boat.speed = 3 + sin(gamma); + Boat.hour = abs(cos(theta)) * 24; + Boat.minute = 30 + sin(theta / 2) * 30; + Boat.seconds = 30 + sin(gamma) * 30; + Boat.milliseconds = 500 + sin(gamma) * 500; + Boat.year = 1 + abs(sin(theta)) * 25; + Boat.month = 1 + abs(sin(gamma)) * 11; + Boat.day = 1 + abs(sin(gamma)) * 26; + Boat.satellites = abs(cos(gamma)) * 10; +#ifdef NMEA_EXTENSIONS + // add some data to the new NMEA data values + Boat.newDataValue(NMEA_AWS, 10 + cos(theta)); + Boat.newDataValue(NMEA_AWA, 180 * sin(gamma)); + Boat.newDataValue(NMEA_VTW, Boat.speed + cos(gamma) / 3); + Boat.newDataValue(NMEA_DEPTH, 10 + cos(gamma) * 5); + Boat.newDataValue(NMEA_HDG, 180 * sin(gamma) + 180); + Boat.newDataValue(NMEA_HDT, 180 * cos(gamma) + 180); + Boat.newDataValue(NMEA_VMG, sin(gamma) * 3); + Boat.newDataValue(NMEA_VMGWP, cos(gamma) * 5); +#endif // NMEA_EXTENSIONS +} + +#ifdef NMEA_EXTENSIONS +void addHistory(Adafruit_GPS *nmea) { + // Record integer history for HDOP, scaled by 10.0, offset by 0.0, + // every 15 seconds for the most recent 20 values. + nmea->initHistory(NMEA_HDOP, 10.0, 0.0, 15, 20); + nmea->initHistory(NMEA_COG, 10.0, 0.0, 1); + nmea->initHistory(NMEA_AWA, 10.0, 0.0, 1); + nmea->initHistory(NMEA_HDG, 10.0, 0.0, 3); + // Record pressure every 10 minutes, in Pa relative to 1 bar + nmea->initHistory(NMEA_BAROMETER, 1.0, -100000.0, 600); + nmea->initHistory(NMEA_DEPTH, 10.0, 0.0, 3); +} +#endif // NMEA_EXTENSIONS diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..0bc963b --- /dev/null +++ b/keywords.txt @@ -0,0 +1,216 @@ +####################################### +# Syntax Coloring Map For RWS_NMEA +####################################### +# https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification +####################################### +# Datatypes (KEYWORD1) +####################################### + +Adafruit_GPS KEYWORD1 +nmea_float_t KEYWORD1 +nmea_history_t KEYWORD1 +nmea_datavalue_t KEYWORD1 +nmea_index_t KEYWORD1 +nmea_check_t KEYWORD1 +nmea_value_type_t KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +lastNMEA KEYWORD2 +newNMEAreceived KEYWORD2 +common_init KEYWORD2 +sendCommand KEYWORD2 +pause KEYWORD2 +parse KEYWORD2 +parseHex KEYWORD2 +check KEYWORD2 +isEmpty KEYWORD2 +addChecksum KEYWORD2 +boatAngle KEYWORD2 +compassAngle KEYWORD2 +secondsSinceFix KEYWORD2 +secondsSinceTime KEYWORD2 +secondsSinceDate KEYWORD2 +resetSentTime KEYWORD2 +wakeup KEYWORD2 +standby KEYWORD2 +onList KEYWORD2 +parseStr KEYWORD2 +parseCoord KEYWORD2 +newDataValue KEYWORD2 +initDataValue KEYWORD2 +initHistory KEYWORD2 +removeHistory KEYWORD2 +showDataValue KEYWORD2 +get KEYWORD2 +getSmoothed KEYWORD2 +isCompoundAngle KEYWORD2 +waitForSentence KEYWORD2 +LOCUS_StartLogger KEYWORD2 +LOCUS_StopLogger KEYWORD2 +LOCUS_ReadStatus KEYWORD2 +build KEYWORD2 + +####################################### +# Instances (KEYWORD2) +####################################### +seconds KEYWORD2 +milliseconds KEYWORD2 +latitude KEYWORD2 +longitude KEYWORD2 +latitude_fixed KEYWORD2 +longitude_fixed KEYWORD2 +latitudeDegrees KEYWORD2 +longitudeDegrees KEYWORD2 +lat KEYWORD2 +lon KEYWORD2 +geoidheight KEYWORD2 +altitude KEYWORD2 +speed KEYWORD2 +angle KEYWORD2 +magvariation KEYWORD2 +HDOP KEYWORD2 +VDOP KEYWORD2 +PDOP KEYWORD2 +mag KEYWORD2 +fix KEYWORD2 +fixquality KEYWORD2 +fixquality_3d KEYWORD2 +satellites KEYWORD2 +LOCUS_serial KEYWORD2 +LOCUS_records KEYWORD2 +LOCUS_type KEYWORD2 +LOCUS_mode KEYWORD2 +LOCUS_config KEYWORD2 +LOCUS_interval KEYWORD2 +LOCUS_distance KEYWORD2 +LOCUS_speed KEYWORD2 +LOCUS_status KEYWORD2 +LOCUS_percent KEYWORD2 +val KEYWORD2 +depthToKeel KEYWORD2 +depthToTransducer KEYWORD2 +toid KEYWORD2 +fromid KEYWORD2 +txtTXT KEYWORD2 +txtTot KEYWORD2 +txtID KEYWORD2 +txtN KEYWORD2 +thisCheck KEYWORD2 +thisSource KEYWORD2 +thisSentence KEYWORD2 +lastSource KEYWORD2 +lastSentence KEYWORD2 +data KEYWORD2 +lastHistory KEYWORD2 +historyInterval KEYWORD2 +scale KEYWORD2 +offset KEYWORD2 +latest KEYWORD2 +smoothed KEYWORD2 +lastUpdate KEYWORD2 +response KEYWORD2 +type KEYWORD2 +ockam KEYWORD2 +hist KEYWORD2 +label KEYWORD2 +unit KEYWORD2 +fmt KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### +NMEA_EXTENSIONS LITERAL1 +NMEA_EXTRAS LITERAL1 +USE_SW_SERIAL LITERAL1 +GPS_DEFAULT_I2C_ADDR LITERAL1 +GPS_MAX_I2C_TRANSFER LITERAL1 +GPS_MAX_SPI_TRANSFER LITERAL1 +MAXLINELENGTH LITERAL1 +NMEA_BAD LITERAL1 +NMEA_HAS_DOLLAR LITERAL1 +NMEA_HAS_CHECKSUM LITERAL1 +NMEA_HAS_NAME LITERAL1 +NMEA_HAS_SOURCE LITERAL1 +NMEA_HAS_SENTENCE LITERAL1 +NMEA_HAS_SENTENCE_P LITERAL1 + +NMEA_SIMPLE_FLOAT LITERAL1 +NMEA_COMPASS_ANGLE LITERAL1 +NMEA_BOAT_ANGLE LITERAL1 +NMEA_COMPASS_ANGLE_SIN LITERAL1 +NMEA_BOAT_ANGLE_SIN LITERAL1 +NMEA_DDMM LITERAL1 +NMEA_HHMMSS LITERAL1 +NMEA_MAX_WP_ID LITERAL1 +NMEA_MAX_SENTENCE_ID LITERAL1 +NMEA_MAX_SOURCE_ID LITERAL1 +NMEA_N_HIST LITERAL1 +DEG_RAD LITERAL1 +NMEA_HDOP LITERAL1 +NMEA_LAT LITERAL1 +NMEA_LON LITERAL1 +NMEA_LATWP LITERAL1 +NMEA_LONWP LITERAL1 +NMEA_SOG LITERAL1 +NMEA_COG LITERAL1 +NMEA_COG_SIN LITERAL1 +NMEA_COG_COS LITERAL1 +NMEA_COGWP LITERAL1 +NMEA_XTE LITERAL1 +NMEA_DISTWP LITERAL1 +NMEA_AWA LITERAL1 +NMEA_AWA_SIN LITERAL1 +NMEA_AWA_COS LITERAL1 +NMEA_AWS LITERAL1 +NMEA_TWA LITERAL1 +NMEA_TWA_SIN LITERAL1 +NMEA_TWA_COS LITERAL1 +NMEA_TWD LITERAL1 +NMEA_TWD_SIN LITERAL1 +NMEA_TWD_COS LITERAL1 +NMEA_TWS LITERAL1 +NMEA_VMG LITERAL1 +NMEA_VMGWP LITERAL1 +NMEA_HEEL LITERAL1 +NMEA_PITCH LITERAL1 +NMEA_HDG LITERAL1 +NMEA_HDG_SIN LITERAL1 +NMEA_HDG_COS LITERAL1 +NMEA_HDT LITERAL1 +NMEA_HDT_SIN LITERAL1 +NMEA_HDT_COS LITERAL1 +NMEA_VTW LITERAL1 +NMEA_LOG LITERAL1 +NMEA_LOGR LITERAL1 +NMEA_DEPTH LITERAL1 +NMEA_RPM_M1 LITERAL1 +NMEA_TEMPERATURE_M1 LITERAL1 +NMEA_PRESSURE_M1 LITERAL1 +NMEA_VOLTAGE_M1 LITERAL1 +NMEA_CURRENT_M1 LITERAL1 +NMEA_RPM_M2 LITERAL1 +NMEA_TEMPERATURE_M2 LITERAL1 +NMEA_PRESSURE_M2 LITERAL1 +NMEA_VOLTAGE_M2 LITERAL1 +NMEA_CURRENT_M2 LITERAL1 +NMEA_TEMPERATURE_AIR LITERAL1 +NMEA_TEMPERATURE_WATER LITERAL1 +NMEA_HUMIDITY LITERAL1 +NMEA_BAROMETER LITERAL1 +NMEA_USR_00 LITERAL1 +NMEA_USR_01 LITERAL1 +NMEA_USR_02 LITERAL1 +NMEA_USR_03 LITERAL1 +NMEA_USR_04 LITERAL1 +NMEA_USR_05 LITERAL1 +NMEA_USR_06 LITERAL1 +NMEA_USR_07 LITERAL1 +NMEA_USR_08 LITERAL1 +NMEA_USR_09 LITERAL1 +NMEA_USR_10 LITERAL1 +NMEA_USR_11 LITERAL1 +NMEA_USR_12 LITERAL1 +NMEA_MAX_INDEX LITERAL1 diff --git a/src/Adafruit_GPS.cpp b/src/Adafruit_GPS.cpp index baec1e7..65e5c10 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,77 +40,30 @@ 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 Add *CS where CS is the two character hex checksum for all but - the first character in the string. The checksum is the result of an - exclusive or of all the characters in the string. Also useful if you - are creating new PMTK strings for controlling a GPS module and need a - checksum added. - @param buff Pointer to the string, which must be long enough - @return none -*/ -/**************************************************************************/ -void Adafruit_GPS::addChecksum(char *buff) { - char cs = 0; - int i = 1; - while (buff[i]) { - cs ^= buff[i]; - i++; - } - sprintf(buff, "%s*%02X", buff, cs); -} - -/**************************************************************************/ -/*! - @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 @param p Pointer to the location of the token in the NMEA string */ /**************************************************************************/ -void Adafruit_GPS::parseLat(char *p) { - char degreebuff[10]; - if (!isEmpty(p)) { - strncpy(degreebuff, p, 2); - p += 2; - degreebuff[2] = '\0'; - long degree = atol(degreebuff) * 10000000; - strncpy(degreebuff, p, 2); // minutes - p += 3; // skip decimal point - strncpy(degreebuff + 2, p, 4); - degreebuff[6] = '\0'; - long minutes = 50 * atol(degreebuff) / 3; - latitude_fixed = degree + minutes; - latitude = degree / 100000 + minutes * 0.000006F; - latitudeDegrees = (latitude - 100 * int(latitude / 100)) / 60.0f; - latitudeDegrees += int(latitude / 100); - } -} +// void Adafruit_GPS::parseLat(char *p) { +// char degreebuff[10]; +// if (!isEmpty(p)) { +// strncpy(degreebuff, p, 2); +// p += 2; +// degreebuff[2] = '\0'; +// long degree = atol(degreebuff) * 10000000; +// strncpy(degreebuff, p, 2); // minutes +// p += 3; // skip decimal point +// strncpy(degreebuff + 2, p, 4); +// degreebuff[6] = '\0'; +// long minutes = 50 * atol(degreebuff) / 3; +// latitude_fixed = degree + minutes; +// latitude = degree / 100000 + minutes * 0.000006F; +// latitudeDegrees = (latitude - 100 * int(latitude / 100)) / 60.0f; +// latitudeDegrees += int(latitude / 100); +// } +// } /**************************************************************************/ /*! @@ -230,20 +72,20 @@ void Adafruit_GPS::parseLat(char *p) { @return True if we parsed it, false if it has invalid data */ /**************************************************************************/ -bool Adafruit_GPS::parseLatDir(char *p) { - if (p[0] == 'S') { - lat = 'S'; - latitudeDegrees *= -1.0f; - latitude_fixed *= -1; - } else if (p[0] == 'N') { - lat = 'N'; - } else if (p[0] == ',') { - lat = 0; - } else { - return false; - } - return true; -} +// bool Adafruit_GPS::parseLatDir(char *p) { +// if (p[0] == 'S') { +// lat = 'S'; +// latitudeDegrees *= -1.0f; +// latitude_fixed *= -1; +// } else if (p[0] == 'N') { +// lat = 'N'; +// } else if (p[0] == ',') { +// lat = 0; +// } else { +// return false; +// } +// return true; +// } /**************************************************************************/ /*! @@ -251,26 +93,26 @@ bool Adafruit_GPS::parseLatDir(char *p) { @param p Pointer to the location of the token in the NMEA string */ /**************************************************************************/ -void Adafruit_GPS::parseLon(char *p) { - int32_t degree; - long minutes; - char degreebuff[10]; - if (!isEmpty(p)) { - strncpy(degreebuff, p, 3); - p += 3; - degreebuff[3] = '\0'; - degree = atol(degreebuff) * 10000000; - strncpy(degreebuff, p, 2); // minutes - p += 3; // skip decimal point - strncpy(degreebuff + 2, p, 4); - degreebuff[6] = '\0'; - minutes = 50 * atol(degreebuff) / 3; - longitude_fixed = degree + minutes; - longitude = degree / 100000 + minutes * 0.000006F; - longitudeDegrees = (longitude - 100 * int(longitude / 100)) / 60.0f; - longitudeDegrees += int(longitude / 100); - } -} +// void Adafruit_GPS::parseLon(char *p) { +// int32_t degree; +// long minutes; +// char degreebuff[10]; +// if (!isEmpty(p)) { +// strncpy(degreebuff, p, 3); +// p += 3; +// degreebuff[3] = '\0'; +// degree = atol(degreebuff) * 10000000; +// strncpy(degreebuff, p, 2); // minutes +// p += 3; // skip decimal point +// strncpy(degreebuff + 2, p, 4); +// degreebuff[6] = '\0'; +// minutes = 50 * atol(degreebuff) / 3; +// longitude_fixed = degree + minutes; +// longitude = degree / 100000 + minutes * 0.000006F; +// longitudeDegrees = (longitude - 100 * int(longitude / 100)) / 60.0f; +// longitudeDegrees += int(longitude / 100); +// } +// } /**************************************************************************/ /*! @@ -279,81 +121,163 @@ void Adafruit_GPS::parseLon(char *p) { @return True if we parsed it, false if it has invalid data */ /**************************************************************************/ -bool Adafruit_GPS::parseLonDir(char *p) { - if (!isEmpty(p)) { - if (p[0] == 'W') { - lon = 'W'; - longitudeDegrees *= -1.0f; - longitude_fixed *= -1; - } else if (p[0] == 'E') { - lon = 'E'; - } else if (p[0] == ',') { - lon = 0; +// bool Adafruit_GPS::parseLonDir(char *p) { +// if (!isEmpty(p)) { +// if (p[0] == 'W') { +// lon = 'W'; +// longitudeDegrees *= -1.0f; +// longitude_fixed *= -1; +// } else if (p[0] == 'E') { +// lon = 'E'; +// } else if (p[0] == ',') { +// lon = 0; +// } else { +// return false; +// } +// } +// return true; +// } + +/**************************************************************************/ +/*! + @brief Start the HW or SW serial port + @param baud_or_i2caddr Baud rate if using serial, I2C address if using I2C + @returns True on successful hardware init, False on failure +*/ +/**************************************************************************/ +bool Adafruit_GPS::begin(uint32_t baud_or_i2caddr) { +#if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) + if (gpsSwSerial) { + gpsSwSerial->begin(baud_or_i2caddr); + } +#endif + if (gpsHwSerial) { + gpsHwSerial->begin(baud_or_i2caddr); + } + if (gpsI2C) { + gpsI2C->begin(); + if (baud_or_i2caddr > 0x7F) { + _i2caddr = GPS_DEFAULT_I2C_ADDR; } else { - return false; + _i2caddr = baud_or_i2caddr; + } + // A basic scanner, see if it ACK's + gpsI2C->beginTransmission(_i2caddr); + return (gpsI2C->endTransmission() == 0); + } + if (gpsSPI) { + gpsSPI->begin(); + gpsSPI_settings = SPISettings(baud_or_i2caddr, MSBFIRST, SPI_MODE0); + if (gpsSPI_cs >= 0) { + pinMode(gpsSPI_cs, OUTPUT); + digitalWrite(gpsSPI_cs, HIGH); } } + + delay(10); 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 + @brief Constructor when using SoftwareSerial + @param ser Pointer to SoftwareSerial device */ /**************************************************************************/ -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; +#if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) +Adafruit_GPS::Adafruit_GPS(SoftwareSerial *ser) { + common_init(); // Set everything to common state, then... + gpsSwSerial = ser; // ...override gpsSwSerial with value passed. +} +#endif + +/**************************************************************************/ +/*! + @brief Constructor when using HardwareSerial + @param ser Pointer to a HardwareSerial object +*/ +/**************************************************************************/ +Adafruit_GPS::Adafruit_GPS(HardwareSerial *ser) { + common_init(); // Set everything to common state, then... + gpsHwSerial = ser; // ...override gpsHwSerial with value passed. } /**************************************************************************/ /*! - @brief Time in seconds since the last position fix was obtained. Will - fail by rolling over to zero after one millis() cycle, about 6-1/2 weeks. - @return nmea_float_t value in seconds since last fix. + @brief Constructor when using I2C + @param theWire Pointer to an I2C TwoWire object */ /**************************************************************************/ -nmea_float_t Adafruit_GPS::secondsSinceFix() { - return (millis() - lastFix) / 1000.; +Adafruit_GPS::Adafruit_GPS(TwoWire *theWire) { + common_init(); // Set everything to common state, then... + gpsI2C = theWire; // ...override gpsI2C } /**************************************************************************/ /*! - @brief Time in seconds since the last GPS time was obtained. Will fail - by rolling over to zero after one millis() cycle, about 6-1/2 weeks. - @return nmea_float_t value in seconds since last GPS time. + @brief Constructor when using SPI + @param theSPI Pointer to an SPI device object + @param cspin The pin connected to the GPS CS, can be -1 if unused */ /**************************************************************************/ -nmea_float_t Adafruit_GPS::secondsSinceTime() { - return (millis() - lastTime) / 1000.; +Adafruit_GPS::Adafruit_GPS(SPIClass *theSPI, int8_t cspin) { + common_init(); // Set everything to common state, then... + gpsSPI = theSPI; // ...override gpsSPI + gpsSPI_cs = cspin; } /**************************************************************************/ /*! - @brief Time in seconds since the last GPS date was obtained. Will fail - by rolling over to zero after one millis() cycle, about 6-1/2 weeks. - @return nmea_float_t value in seconds since last GPS date. + @brief Constructor when there are no communications attached */ /**************************************************************************/ -nmea_float_t Adafruit_GPS::secondsSinceDate() { - return (millis() - lastDate) / 1000.; +Adafruit_GPS::Adafruit_GPS() { + common_init(); // Set everything to common state, then... + noComms = true; } /**************************************************************************/ /*! - @brief Fakes time of receipt of a sentence. Use between build() and parse() - to make the timing look like the sentence arrived from the GPS. + @brief Initialization code used by all constructor types */ /**************************************************************************/ -void Adafruit_GPS::resetSentTime() { sentTime = millis(); } +void Adafruit_GPS::common_init(void) { +#if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) + gpsSwSerial = NULL; // Set both to NULL, then override correct +#endif + gpsHwSerial = NULL; // port pointer in corresponding constructor + gpsI2C = NULL; + gpsSPI = NULL; + recvdflag = false; + paused = false; + lineidx = 0; + currentline = line1; + lastline = line2; + + hour = minute = seconds = year = month = day = fixquality = fixquality_3d = + satellites = 0; // uint8_t + lat = lon = mag = 0; // char + fix = false; // bool + milliseconds = 0; // uint16_t + latitude = longitude = geoidheight = altitude = speed = angle = magvariation = + HDOP = VDOP = PDOP = 0.0; // nmea_float_t +#ifdef NMEA_EXTENSIONS + data_init(); +#endif +} + +/**************************************************************************/ +/*! + @brief Destroy the object. + @return none +*/ +/**************************************************************************/ +Adafruit_GPS::~Adafruit_GPS() { +#ifdef NMEA_EXTENSIONS + for (int i = 0; i < (int)NMEA_MAX_INDEX; i++) + removeHistory((nmea_index_t)i); // to free any history mallocs +#endif +} /**************************************************************************/ /*! @@ -434,7 +358,7 @@ char Adafruit_GPS::read(void) { uint32_t tStart = millis(); // as close as we can get to time char was sent char c = 0; - if (paused) + if (paused || noComms) return c; #if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) @@ -530,121 +454,6 @@ char Adafruit_GPS::read(void) { return c; } -/**************************************************************************/ -/*! - @brief Constructor when using SoftwareSerial - @param ser Pointer to SoftwareSerial device -*/ -/**************************************************************************/ -#if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) -Adafruit_GPS::Adafruit_GPS(SoftwareSerial *ser) { - common_init(); // Set everything to common state, then... - gpsSwSerial = ser; // ...override gpsSwSerial with value passed. -} -#endif - -/**************************************************************************/ -/*! - @brief Constructor when using HardwareSerial - @param ser Pointer to a HardwareSerial object -*/ -/**************************************************************************/ -Adafruit_GPS::Adafruit_GPS(HardwareSerial *ser) { - common_init(); // Set everything to common state, then... - gpsHwSerial = ser; // ...override gpsHwSerial with value passed. -} - -/**************************************************************************/ -/*! - @brief Constructor when using I2C - @param theWire Pointer to an I2C TwoWire object -*/ -/**************************************************************************/ -Adafruit_GPS::Adafruit_GPS(TwoWire *theWire) { - common_init(); // Set everything to common state, then... - gpsI2C = theWire; // ...override gpsI2C -} - -/**************************************************************************/ -/*! - @brief Constructor when using SPI - @param theSPI Pointer to an SPI device object - @param cspin The pin connected to the GPS CS, can be -1 if unused -*/ -/**************************************************************************/ -Adafruit_GPS::Adafruit_GPS(SPIClass *theSPI, int8_t cspin) { - common_init(); // Set everything to common state, then... - gpsSPI = theSPI; // ...override gpsSPI - gpsSPI_cs = cspin; -} - -/**************************************************************************/ -/*! - @brief Initialization code used by all constructor types -*/ -/**************************************************************************/ -void Adafruit_GPS::common_init(void) { -#if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) - gpsSwSerial = NULL; // Set both to NULL, then override correct -#endif - gpsHwSerial = NULL; // port pointer in corresponding constructor - gpsI2C = NULL; - gpsSPI = NULL; - recvdflag = false; - paused = false; - lineidx = 0; - currentline = line1; - lastline = line2; - - hour = minute = seconds = year = month = day = fixquality = fixquality_3d = - satellites = 0; // uint8_t - lat = lon = mag = 0; // char - fix = false; // bool - milliseconds = 0; // uint16_t - latitude = longitude = geoidheight = altitude = speed = angle = magvariation = - HDOP = VDOP = PDOP = 0.0; // nmea_float_t -} - -/**************************************************************************/ -/*! - @brief Start the HW or SW serial port - @param baud_or_i2caddr Baud rate if using serial, I2C address if using I2C - @returns True on successful hardware init, False on failure -*/ -/**************************************************************************/ -bool Adafruit_GPS::begin(uint32_t baud_or_i2caddr) { -#if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) - if (gpsSwSerial) { - gpsSwSerial->begin(baud_or_i2caddr); - } -#endif - if (gpsHwSerial) { - gpsHwSerial->begin(baud_or_i2caddr); - } - if (gpsI2C) { - gpsI2C->begin(); - if (baud_or_i2caddr > 0x7F) { - _i2caddr = GPS_DEFAULT_I2C_ADDR; - } else { - _i2caddr = baud_or_i2caddr; - } - // A basic scanner, see if it ACK's - gpsI2C->beginTransmission(_i2caddr); - return (gpsI2C->endTransmission() == 0); - } - if (gpsSPI) { - gpsSPI->begin(); - gpsSPI_settings = SPISettings(baud_or_i2caddr, MSBFIRST, SPI_MODE0); - if (gpsSPI_cs >= 0) { - pinMode(gpsSPI_cs, OUTPUT); - digitalWrite(gpsSPI_cs, HIGH); - } - } - - delay(10); - return true; -} - /**************************************************************************/ /*! @brief Send a command to the GPS device @@ -680,28 +489,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 @@ -842,6 +629,47 @@ bool Adafruit_GPS::wakeup(void) { } } +/**************************************************************************/ +/*! + @brief Time in seconds since the last position fix was obtained. Will + fail by rolling over to zero after one millis() cycle, about 6-1/2 weeks. + @return nmea_float_t value in seconds since last fix. +*/ +/**************************************************************************/ +nmea_float_t Adafruit_GPS::secondsSinceFix() { + return (millis() - lastFix) / 1000.; +} + +/**************************************************************************/ +/*! + @brief Time in seconds since the last GPS time was obtained. Will fail + by rolling over to zero after one millis() cycle, about 6-1/2 weeks. + @return nmea_float_t value in seconds since last GPS time. +*/ +/**************************************************************************/ +nmea_float_t Adafruit_GPS::secondsSinceTime() { + return (millis() - lastTime) / 1000.; +} + +/**************************************************************************/ +/*! + @brief Time in seconds since the last GPS date was obtained. Will fail + by rolling over to zero after one millis() cycle, about 6-1/2 weeks. + @return nmea_float_t value in seconds since last GPS date. +*/ +/**************************************************************************/ +nmea_float_t Adafruit_GPS::secondsSinceDate() { + return (millis() - lastDate) / 1000.; +} + +/**************************************************************************/ +/*! + @brief Fakes time of receipt of a sentence. Use between build() and parse() + to make the timing look like the sentence arrived from the GPS. +*/ +/**************************************************************************/ +void Adafruit_GPS::resetSentTime() { sentTime = millis(); } + /**************************************************************************/ /*! @brief Checks whether a string starts with a specified prefix diff --git a/src/Adafruit_GPS.h b/src/Adafruit_GPS.h index 145fce5..fe9baee 100644 --- a/src/Adafruit_GPS.h +++ b/src/Adafruit_GPS.h @@ -21,8 +21,6 @@ */ /**************************************************************************/ -// Fllybob added lines 34,35 and 40,41 to add 100mHz logging capability - #ifndef _ADAFRUIT_GPS_H #define _ADAFRUIT_GPS_H @@ -31,9 +29,15 @@ Comment out the definition of NMEA_EXTENSIONS to make the library use as little memory as possible for GPS functionality only. The ARDUINO_ARCH_AVR test should leave it out of any compilations for the UNO and similar. */ +#ifndef NMEA_EXTRAS // inject on the compile command line to force extensions #ifndef ARDUINO_ARCH_AVR #define NMEA_EXTENSIONS ///< if defined will include more NMEA sentences #endif +#else +#if (NMEA_EXTRAS > 0) +#define NMEA_EXTENSIONS ///< if defined will include more NMEA sentences +#endif +#endif #define USE_SW_SERIAL ///< comment this out if you don't want to include ///< software serial in the library @@ -60,8 +64,9 @@ /// type for resulting code from running check() typedef enum { - NMEA_BAD = 0, ///< passed none of the checks - NMEA_HAS_DOLLAR = 1, ///< has a dollar sign in the first position + NMEA_BAD = 0, ///< passed none of the checks + NMEA_HAS_DOLLAR = + 1, ///< has a dollar sign or exclamation mark in the first position NMEA_HAS_CHECKSUM = 2, ///< has a valid checksum at the end NMEA_HAS_NAME = 4, ///< there is a token after the $ followed by a comma NMEA_HAS_SOURCE = 10, ///< has a recognized source ID @@ -75,6 +80,7 @@ typedef enum { */ class Adafruit_GPS : public Print { public: + // Adafruit_GPS.cpp bool begin(uint32_t baud_or_i2caddr); #if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) @@ -83,31 +89,60 @@ public: Adafruit_GPS(HardwareSerial *ser); // Constructor when using HardwareSerial Adafruit_GPS(TwoWire *theWire); // Constructor when using I2C Adafruit_GPS(SPIClass *theSPI, int8_t cspin); // Constructor when using SPI - - char *lastNMEA(void); - bool newNMEAreceived(); + Adafruit_GPS(); // Constructor for no communications, just data storage void common_init(void); + virtual ~Adafruit_GPS(); - void sendCommand(const char *); - - 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 *); - void addChecksum(char *buff); + size_t write(uint8_t); + char read(void); + void sendCommand(const char *); + bool newNMEAreceived(); + void pause(bool b); + char *lastNMEA(void); + bool waitForSentence(const char *wait, uint8_t max = MAXWAITSENTENCE, + bool usingInterrupts = false); + bool LOCUS_StartLogger(void); + bool LOCUS_StopLogger(void); + bool LOCUS_ReadStatus(void); + bool standby(void); + bool wakeup(void); nmea_float_t secondsSinceFix(); nmea_float_t secondsSinceTime(); nmea_float_t secondsSinceDate(); void resetSentTime(); - 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, + char ref = 'R'); +#endif + void addChecksum(char *buff); + + // NMEA_data.cpp + void newDataValue(nmea_index_t tag, nmea_float_t v); +#ifdef NMEA_EXTENSIONS + nmea_float_t get(nmea_index_t idx); + nmea_float_t getSmoothed(nmea_index_t idx); + void initDataValue(nmea_index_t idx, char *label = NULL, char *fmt = NULL, + char *unit = NULL, unsigned long response = 0, + nmea_value_type_t type = NMEA_SIMPLE_FLOAT); + nmea_history_t *initHistory(nmea_index_t idx, nmea_float_t scale = 10.0, + nmea_float_t offset = 0.0, + unsigned historyInterval = 20, + unsigned historyN = 192); + void removeHistory(nmea_index_t idx); + void showDataValue(nmea_index_t idx, int n = 7); + bool isCompoundAngle(nmea_index_t idx); +#endif + nmea_float_t boatAngle(nmea_float_t s, nmea_float_t c); + nmea_float_t compassAngle(nmea_float_t s, nmea_float_t c); int thisCheck = 0; ///< the results of the check on the current sentence char thisSource[NMEA_MAX_SOURCE_ID] = { @@ -162,12 +197,6 @@ public: uint8_t fixquality_3d; ///< 3D fix quality (1, 3, 3 = Nofix, 2D fix, 3D fix) uint8_t satellites; ///< Number of satellites in use - bool waitForSentence(const char *wait, uint8_t max = MAXWAITSENTENCE, - bool usingInterrupts = false); - bool LOCUS_StartLogger(void); - bool LOCUS_StopLogger(void); - bool LOCUS_ReadStatus(void); - uint16_t LOCUS_serial; ///< Log serial number uint16_t LOCUS_records; ///< Log number of data record uint8_t LOCUS_type; ///< Log type, 0: Overlap, 1: FullStop @@ -180,11 +209,20 @@ public: uint8_t LOCUS_percent; ///< Log life used percentage #ifdef NMEA_EXTENSIONS - // NMEA additional public functions - char *build(char *nmea, const char *thisSource, const char *thisSentence, - char ref = 'R'); - // NMEA additional public variables + nmea_datavalue_t + val[NMEA_MAX_INDEX]; ///< an array of data value structs, val[0] = most + ///< recent HDOP so that ockam indexing works + nmea_float_t depthToKeel = + 2.4; ///< depth from surface to bottom of keel in metres + nmea_float_t depthToTransducer = + 0.0; ///< depth of transducer below the surface in metres + + char toID[NMEA_MAX_WP_ID] = { + 0}; ///< id of waypoint going to on this segment of the route + char fromID[NMEA_MAX_WP_ID] = { + 0}; ///< id of waypoint coming from on this segment of the route + char txtTXT[63] = {0}; ///< text content from most recent TXT sentence int txtTot = 0; ///< total TXT sentences in group int txtID = 0; ///< id of the text message @@ -192,22 +230,41 @@ public: #endif // NMEA_EXTENSIONS private: + // 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 isEmpty(char *pStart); - void parseTime(char *); - void parseLat(char *); - bool parseLatDir(char *); - void parseLon(char *); - bool parseLonDir(char *); + bool parseTime(char *); bool parseFix(char *); + bool isEmpty(char *pStart); + // used by check() for validity tests, room for future expansion - const char *sources[5] = {"II", "WI", "GP", "GN", - "ZZZ"}; ///< valid source ids + const char *sources[6] = {"II", "WI", "GP", + "GN", "P", "ZZZ"}; ///< valid source ids +#ifdef NMEA_EXTENSIONS + const char + *sentences_parsed[20] = + { + "GGA", "GLL", "GSA", "RMC", "DBT", "HDM", "HDT", + "MDA", "MTW", "MWV", "RMB", "TXT", "VHW", "VLW", + "VPW", "VWR", "WCV", "XTE", "ZZZ"}; ///< parseable sentence ids + const char *sentences_known[15] = { + "APB", "DPT", "GSV", "HDG", "MWD", "ROT", + "RPM", "RSA", "VDR", "VTG", "ZDA", "ZZZ"}; ///< known, but not parseable +#else // make the lists short to save memory const char *sentences_parsed[5] = {"GGA", "GLL", "GSA", "RMC", "ZZZ"}; ///< parseable sentence ids - const char *sentences_known[1] = { - "ZZZ"}; ///< known, but not parseable sentence ids + const char *sentences_known[4] = {"DBT", "HDM", "HDT", + "ZZZ"}; ///< known, but not parseable +#endif // Make all of these times far in the past by setting them near the middle of // the millis() range. Timing assumes that sentences are parsed promptly. @@ -226,6 +283,7 @@ private: #if (defined(__AVR__) || defined(ESP8266)) && defined(USE_SW_SERIAL) SoftwareSerial *gpsSwSerial; #endif + bool noComms = false; HardwareSerial *gpsHwSerial; TwoWire *gpsI2C; SPIClass *gpsSPI; diff --git a/src/NMEA_build.cpp b/src/NMEA_build.cpp index fc2a4a1..8b78139 100644 --- a/src/NMEA_build.cpp +++ b/src/NMEA_build.cpp @@ -45,10 +45,16 @@ build() will not work properly in an environment that does not support the %f floating point formatter in sprintf(), and will return NULL. + Floating point arguments to sprintf() are explicitly cast to double to + avoid warnings in some compilers. build() adds Carriage Return and Line Feed to sentences to conform to NMEA-183, so send your output with a print, not a println. + The resulting sentence may be corrupted if the input data is corrupt. + In particular, the sentence will be truncated if any of the character + data is 0, e.g. if lat is not set to 'N' or 'S'. + Some of the data in these test sentences may be arbitrary, e.g. for the TXT sentence which has a more complicated protocol for multiple lines sent as a message set. Also, the data in the class variables are presumed @@ -66,7 +72,8 @@ /**************************************************************************/ char *Adafruit_GPS::build(char *nmea, const char *thisSource, const char *thisSentence, char ref) { - sprintf(nmea, "%6.2f", 123.45); // fail if sprintf() doesn't handle floats + sprintf(nmea, "%6.2f", + (double)123.45); // fail if sprintf() doesn't handle floats if (strcmp(nmea, "123.45")) return NULL; *nmea = '$'; @@ -82,8 +89,7 @@ char *Adafruit_GPS::build(char *nmea, const char *thisSource, // pruning excess code easier. Otherwise, keep them alphabetical for ease of // reading. - if (!strcmp(thisSentence, - "GGA")) { //********************************************GGA + if (!strcmp(thisSentence, "GGA")) { //************************************GGA // GGA Global Positioning System Fix Data. Time, Position and fix related // data for a GPS receiver // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @@ -107,12 +113,12 @@ char *Adafruit_GPS::build(char *nmea, const char *thisSource, // 14) Differential reference station ID, 0000-1023 // 15) Checksum sprintf(p, "%09.2f,%09.4f,%c,%010.4f,%c,%d,%02d,%f,%f,M,%f,M,,", - hour * 10000L + minute * 100L + seconds + milliseconds / 1000., + (double)hour * 10000L + minute * 100L + seconds + + milliseconds / 1000., (double)latitude, lat, (double)longitude, lon, fixquality, satellites, (double)HDOP, (double)altitude, (double)geoidheight); - } else if (!strcmp(thisSentence, - "GLL")) { //********************************************GLL + } else if (!strcmp(thisSentence, "GLL")) { //*****************************GLL // GLL Geographic Position – Latitude/Longitude // 1 2 3 4 5 6 7 // | | | | | | | @@ -126,10 +132,10 @@ char *Adafruit_GPS::build(char *nmea, const char *thisSource, // 7) Checksum sprintf(p, "%09.4f,%c,%010.4f,%c,%09.2f,A", (double)latitude, lat, (double)longitude, lon, - hour * 10000L + minute * 100L + seconds + milliseconds / 1000.); + (double)hour * 10000L + minute * 100L + seconds + + milliseconds / 1000.); - } else if (!strcmp(thisSentence, - "GSA")) { //******************************************** + } else if (!strcmp(thisSentence, "GSA")) { //*****************************GSA // GSA GPS DOP and active satellites // 1 2 3 14 15 16 17 18 // | | | | | | | | @@ -146,8 +152,7 @@ char *Adafruit_GPS::build(char *nmea, const char *thisSource, // 18) Checksum return NULL; - } else if (!strcmp(thisSentence, - "RMC")) { //********************************************RMC + } else if (!strcmp(thisSentence, "RMC")) { //*****************************RMC // RMC Recommended Minimum Navigation Information // 12 // 1 2 3 4 5 6 7 8 9 10 11 | @@ -166,13 +171,257 @@ char *Adafruit_GPS::build(char *nmea, const char *thisSource, // 11) E or W // 12) Checksum sprintf(p, "%09.2f,A,%09.4f,%c,%010.4f,%c,%f,%f,%06d,%f,%c", - hour * 10000L + minute * 100L + seconds + milliseconds / 1000., + (double)hour * 10000L + minute * 100L + seconds + + milliseconds / 1000., (double)latitude, lat, (double)longitude, lon, (double)speed, (double)angle, day * 10000 + month * 100 + year, (double)magvariation, mag); - } else if (!strcmp(thisSentence, - "TXT")) { //********************************************TXT + } else if (!strcmp(thisSentence, "APB")) { //*****************************APB + // APB Autopilot Sentence "B" + // 13 15 + // 1 2 3 4 5 6 7 8 9 10 11 12 | 14 | + // | | | | | | | | | | | | | | | + //$--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a*hh + // 1) Status + // V = LORAN-C Blink or SNR warning + // A = general warning flag or other navigation systems when a reliable + // fix is not available + // 2) Status + // V = Loran-C Cycle Lock warning flag + // A = OK or not used + // 3) Cross Track Error Magnitude + // 4) Direction to steer, L or R + // 5) Cross Track Units, N = Nautical Miles + // 6) Status + // A = Arrival Circle Entered + // 7) Status + // A = Perpendicular passed at waypoint + // 8) Bearing origin to destination + // 9) M = Magnetic, T = True + // 10) Destination Waypoint ID + // 11) Bearing, present position to Destination + // 12) M = Magnetic, T = True + // 13) Heading to steer to destination waypoint + // 14) M = Magnetic, T = True + // 15) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "DBK")) { //*****************************DBT + // DBK Depth Below Keel + // 1 2 3 4 5 6 7 + // | | | | | | | + //$--DBK,x.x,f,x.x,M,x.x,F*hh + // 1) Depth, feet + // 2) f = feet + // 3) Depth, meters + // 4) M = meters + // 5) Depth, Fathoms + // 6) F = Fathoms + // 7) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "DBS")) { //*****************************DBT + // DBS Depth Below Surface + // 1 2 3 4 5 6 7 + // | | | | | | | + //$--DBS,x.x,f,x.x,M,x.x,F*hh + // 1) Depth, feet + // 2) f = feet + // 3) Depth, meters + // 4) M = meters + // 5) Depth, Fathoms + // 6) F = Fathoms + // 7) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "DBT")) { //*****************************DBT + // DBT Depth Below Transducer + // 1 2 3 4 5 6 7 + // | | | | | | | + //$--DBT,x.x,f,x.x,M,x.x,F*hh + // 1) Depth, feet + // 2) f = feet + // 3) Depth, meters + // 4) M = meters + // 5) Depth, Fathoms + // 6) F = Fathoms + // 7) Checksum + double d = val[NMEA_DEPTH].latest - depthToTransducer; + sprintf(p, "%f,f,%f,M,,,", d / 0.3048, d); + + } else if (!strcmp(thisSentence, "DPT")) { //*****************************DPT + // DPT Heading – Deviation & Variation + // 1 2 3 + // | | | + //$--DPT,x.x,x.x*hh + // 1) Depth, meters + // 2) Offset from transducer; + // positive means distance from transducer to water line, + // negative means distance from transducer to keel + // 3) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "GSV")) { //*****************************GSV + // GSV Satellites in view + // 1 2 3 4 5 6 7 n + // | | | | | | | | + //$--GSV,x,x,x,x,x,x,x,...*hh + // 1) total number of messages + // 2) message number + // 3) satellites in view + // 4) satellite number + // 5) elevation in degrees + // 6) azimuth in degrees to true + // 7) SNR in dB + // more satellite infos like 4)-7) + // n) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "HDG")) { //*****************************HDG + // HDG Heading – Deviation & Variation + // 1 2 3 4 5 6 + // | | | | | | + //$--HDG,x.x,x.x,a,x.x,a*hh + // 1) Magnetic Sensor heading in degrees + // 2) Magnetic Deviation, degrees + // 3) Magnetic Deviation direction, E = Easterly, W = Westerly + // 4) Magnetic Variation degrees + // 5) Magnetic Variation direction, E = Easterly, W = Westerly + // 6) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "HDM")) { //*****************************HDM + // HDM Heading – Magnetic + // 1 2 3 + // | | | + //$--HDM,x.x,M*hh + // 1) Heading Degrees, magnetic + // 2) M = magnetic + // 3) Checksum + sprintf(p, "%f,M", (double)val[NMEA_HDG].latest); + + } else if (!strcmp(thisSentence, "HDT")) { //*****************************HDT + // HDT Heading – True + // 1 2 3 + // | | | + //$--HDT,x.x,T*hh + // 1) Heading Degrees, true + // 2) T = True + // 3) Checksum + // starts with $II for integrated instrumentation + sprintf(p, "%f,T", (double)val[NMEA_HDT].latest); + + } else if (!strcmp(thisSentence, "MDA")) { //*****************************MDA + // MDA Meteorological Composite + // 1 2 3 4 5 6 7 8 9 10 11 12 + // | | | | | | | | | | | | + //$__MDA,x.x,I,x.x,B,x.x,C,x.x,C,x.x, ,x.x,C,,T,,M,,N,,M*hh + //$IIMDA,,I,,B,,C,21.8,C,,,,C,,T,,M,,N,,M*0F // sent by RayMarine i70s + // Speed/Depth/Wind + // 1) Barometric Pressure + // 2) inches of Hg + // 3) Barometric Pressure + // 4) bar + // 5) Atmospheric Temperature + // 6) C or F + // 7) Water Temperature + // 8) C or F + // 9) Relative Humidity + // 10) + // 11) Dew Point + // 12) C or F + return NULL; + + } else if (!strcmp(thisSentence, "MTW")) { //*****************************MTW + // MTW Water Temperature + // 1 2 3 + // | | | + //$IIMTW,x.x,C*hh + //$IIMTW,21.8,C*18 // sent by RayMarine i70s Speed/Depth/Wind + // 1) Degrees + // 2) Unit of Measurement, Celcius + // 3) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "MWD")) { //*****************************MWD + // MWD Wind Direction & Speed + // Format unknown + return NULL; + + } else if (!strcmp(thisSentence, "MWV")) { //*****************************MWV + // MWV Wind Speed and Angle assuming values for True + // 1 2 3 4 5 6 + // | | | | | | + //$IIMWV,x.x,a,x.x,a,a*hh + //$WIMWV,276.94,R,0,N,A*03 // sent by RayMarine i70s Speed/Depth/Wind + // 1) Wind Angle, 0 to 360 degrees + // 2) Reference, R = Relative, T = True + // 3) Wind Speed + // 4) Wind Speed Units, K/M/N kilometers/miles/knots + // 5) Status, A = Data Valid + // 6) Checksum + if (ref == 'R') + sprintf(p, "%f,%c,%f,N,A", (double)val[NMEA_AWA].latest, ref, + (double)val[NMEA_AWS].latest); + else + sprintf(p, "%f,%c,%f,N,A", (double)val[NMEA_TWA].latest, 'T', + (double)val[NMEA_TWS].latest); + + } else if (!strcmp(thisSentence, "RMB")) { //*****************************RMB + // RMB Recommended Minimum Navigation Information + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + // | | | | | | | | | | | | | | + //$--RMB,A,x.x,a,c--c,c--c,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,A*hh + // 1) Status, V = Navigation receiver warning + // 2) Cross Track error - nautical miles + // 3) Direction to Steer, Left or Right + // 4) TO Waypoint ID + // 5) FROM Waypoint ID + // 6) Destination Waypoint Latitude 7) N or S + // 8) Destination Waypoint Longitude 9) E or W + // 10) Range to destination in nautical miles + // 11) Bearing to destination in degrees True + // 12) Destination closing velocity in knots + // 13) Arrival Status, A = Arrival Circle Entered 14) Checksum + sprintf(p, ",,,,,,,,,,,%f,A", (double)val[NMEA_VMGWP].latest); + + } else if (!strcmp(thisSentence, "ROT")) { //*****************************ROT + // ROT Rate Of Turn + // 1 2 3 + // | | | + //$--ROT,x.x,A*hh + // 1) Rate Of Turn, degrees per minute, "-" means bow turns to port + // 2) Status, A means data is valid + // 3) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "RPM")) { //*****************************RPM + // RPM Revolutions + // 1 2 3 4 5 6 + // | | | | | | + //$--RPM,a,x,x.x,x.x,A*hh + // 1) Source; S = Shaft, E = Engine + // 2) Engine or shaft number + // 3) Speed, Revolutions per minute + // 4) Propeller pitch, % of maximum, "-" means astern + // 5) Status, A means data is valid + // 6) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "RSA")) { //*****************************RSA + // RSA Rudder Sensor Angle + // 1 2 3 4 5 + // | | | | | + //$--RSA,x.x,A,x.x,A*hh + // 1) Starboard (or single) rudder sensor, "-" means Turn To Port + // 2) Status, A means data is valid + // 3) Port rudder sensor + // 4) Status, A means data is valid + // 5) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "TXT")) { //*****************************TXT // as mentioned in https://github.com/adafruit/Adafruit_GPS/issues/95 // TXT Text Transmission // 1 2 3 4 5 @@ -185,6 +434,136 @@ char *Adafruit_GPS::build(char *nmea, const char *thisSource, // 5) Checksum sprintf(p, "01,01,23,This is the text of the sample message"); + } else if (!strcmp(thisSentence, "VDR")) { //*****************************VDR + // VDR Set and Drift + // 1 2 3 4 5 6 7 + // | | | | | | | + //$--VDR,x.x,T,x.x,M,x.x,N*hh + // 1) Degress True + // 2) T = True + // 3) Degrees Magnetic + // 4) M = Magnetic + // 5) Knots (speed of current) + // 6) N = Knots + // 7) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "VHW")) { //*****************************VHW + // VHW Water Speed and Heading + // 1 2 3 4 5 6 7 8 9 + // | | | | | | | | | + //$--VHW,x.x,T,x.x,M,x.x,N,x.x,K*hh + //$IIVHW,,T,,M,0,N,0,K*55 // sent by RayMarine i70s Speed/Depth/Wind + // 1) Degrees True + // 2) T = True + // 3) Degrees Magnetic + // 4) M = Magnetic + // 5) Knots (speed of vessel relative to the water) [66] + // 6) N = Knots + // 7) Kilometers (speed of vessel relative to the water) + // 8) K = Kilometres + // 9) Checksum + sprintf(p, "%f,T,%f,M,%f,N,%f,K", (double)val[NMEA_HDT].latest, + (double)val[NMEA_HDG].latest, (double)val[NMEA_VTW].latest, + (double)val[NMEA_VTW].latest * 1.829); + + } else if (!strcmp(thisSentence, "VLW")) { //*****************************VLW + // VLW Distance Traveled through Water + // 1 2 3 4 5 + // | | | | | + //$--VLW,x.x,N,x.x,N*hh + //$IIVLW,0,N,0,N,,N,,N*4D // sent by RayMarine i70s Speed/Depth/Wind + // not sure what the last two are? + // 1) Total cumulative distance + // 2) N = Nautical Miles + // 3) Distance since Reset + // 4) N = Nautical Miles + // 5) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "VPW")) { //*****************************VPW + // not supported by iNavX + // VPW Speed – Measured Parallel to Wind + // 1 2 3 4 5 + // | | | | | + //$--VPW,x.x,N,x.x,M*hh + // 1) Speed, "-" means downwind + // 2) N = Knots + // 3) Speed, "-" means downwind + // 4) M = Meters per second + // 5) Checksum + sprintf(p, "%f,N,,", (double)val[NMEA_VMG].latest); + + } else if (!strcmp(thisSentence, "VTG")) { //*****************************VTG + // VTG Track Made Good and Ground Speed + // 1 2 3 4 5 6 7 8 9 + // | | | | | | | | | + //$--VTG,x.x,T,x.x,M,x.x,N,x.x,K*hh + // 1) Track Degrees 2) T = True + // 3) Track Degrees 4) M = Magnetic + // 5) Speed Knots 6) N = Knots + // 7) Speed Kilometers Per Hour 8) K = Kilometres Per Hour + // 9) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "VWR")) { //*****************************VWR + // VWR Relative Wind Speed and Angle + // 1 2 3 4 5 6 7 8 9 + // | | | | | | | | | + //$--VWR,x.x,a,x.x,N,x.x,M,x.x,K*hh + //$WIVWR,83.1,L,0,N,0,M,0,K*6D // sent by RayMarine i70s + // Speed/Depth/Wind + // 1) Wind direction magnitude in degrees + // 2) Wind direction Left/Right of bow + // 3) Speed + // 4) N = Knots + // 5) Speed + // 6) M = Meters Per Second + // 7) Speed + // 8) K = Kilometers Per Hour + // 9) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "WCV")) { //*****************************WCV + // WCV Waypoint Closure Velocity + // 1 2 3 4 + // | | | | + //$--WCV,x.x,N,c--c*hh + // 1) Velocity 2) N = knots 3) Waypoint ID 4) Checksum + sprintf(p, "%f,N,home", (double)val[NMEA_VMG].latest); + + } else if (!strcmp(thisSentence, "XTE")) { //*****************************XTE + // XTE Cross-Track Error – Measured + // 1 2 3 4 5 6 + // | | | | | | + //$--XTE,A,A,x.x,a,N,*hh + // 1) Status + // V = LORAN-C blink or SNR warning + // A = general warning flag or other navigation systems when a reliable + // fix is not available + // 2) Status + // V = Loran-C cycle lock warning flag + // A = OK or not used + // 3) Cross track error magnitude + // 4) Direction to steer, L or R + // 5) Cross track units. N = Nautical Miles + // 6) Checksum + return NULL; + + } else if (!strcmp(thisSentence, "ZDA")) { //*****************************ZDA + // ZDA Time & Date – UTC, Day, Month, Year and Local Time Zone + // 1 2 3 4 5 6 7 + // | | | | | | | + //$--ZDA,hhmmss.ss,xx,xx,xxxx,xx,xx*hh + // 1) Local zone minutes description, same sign as local hours + // 2) Local zone description, 00 to +/- 13 hours + // 3) Year + // 4) Month, 01 to 12 + // 5) Day, 01 to 31 + // 6) Time (UTC) + // 7) Checksum + return NULL; + } else { return NULL; // didn't find a match for the build request } @@ -196,3 +575,24 @@ char *Adafruit_GPS::build(char *nmea, const char *thisSource, } #endif // NMEA_EXTENSIONS + +/**************************************************************************/ +/*! + @brief Add *CS where CS is the two character hex checksum for all but + the first character in the string. The checksum is the result of an + exclusive or of all the characters in the string. Also useful if you + are creating new PMTK strings for controlling a GPS module and need a + checksum added. + @param buff Pointer to the string, which must be long enough + @return none +*/ +/**************************************************************************/ +void Adafruit_GPS::addChecksum(char *buff) { + char cs = 0; + int i = 1; + while (buff[i]) { + cs ^= buff[i]; + i++; + } + sprintf(buff, "%s*%02X", buff, cs); +} diff --git a/src/NMEA_data.cpp b/src/NMEA_data.cpp new file mode 100644 index 0000000..d3d05ed --- /dev/null +++ b/src/NMEA_data.cpp @@ -0,0 +1,566 @@ +/**************************************************************************/ +/*! + @file NMEA_data.cpp + + @section intro Introduction + + Code for tracking values that change with time so that history can be + examined for recent trends in real time. This code will only generate the + stubs for newDataValue() and data_init(), adding essentially nothing to + the memory footprint unless NMEA_EXTENSIONS is defined. + + This is code intended to complement the Adafruit GPS library and process + data for many additional NMEA sentences, mostly of interest to sailors. + + The parse function can be a direct substitute for the Adafruit_GPS + function of the same name, updating the same variables within an NMEA + object. A simple use case would involve: + + Define an Adafruit_GPS object and use it to collect and parse sentences + from a serial port. The GPS object will be updated and can be used exactly + as usual. + + Define an NMEA object and use it to parse the same sentences. It will + succeed on more sentences than the GPS object and keep more detailed data + records. It updates all the same variables as the GPS object, so you could + skip the GPS parsing step. + + @section author Author + + Written by Rick Sellens. + + @section license License + + CCBY license +*/ +/**************************************************************************/ + +#include "Adafruit_GPS.h" + +/**************************************************************************/ +/*! + @brief Update the value and history information with a new value. Call + whenever a new data value is received. The function does nothing if the + NMEA extensions are not enabled. + @param idx The data index for which a new value has been received + @param v The new value received + @return none +*/ +/**************************************************************************/ +void Adafruit_GPS::newDataValue(nmea_index_t idx, nmea_float_t v) { +#ifdef NMEA_EXTENSIONS + // Serial.println();Serial.print(idx);Serial.print(", "); Serial.println(v); + val[idx].latest = v; // update the value + + // update the smoothed verion + if (isCompoundAngle(idx)) { // angle with sin/cos component recording + newDataValue((nmea_index_t)(idx + 1), sin(v / RAD_TO_DEG)); + newDataValue((nmea_index_t)(idx + 2), cos(v / RAD_TO_DEG)); + } + // weighting factor for smoothing depends on delta t / tau + nmea_float_t w = + min((nmea_float_t)1.0, + ((nmea_float_t)millis() - val[idx].lastUpdate) / val[idx].response); + // default smoothing + val[idx].smoothed = (1.0 - w) * val[idx].smoothed + w * v; + // special smoothing for some angle types + if (val[idx].type == NMEA_COMPASS_ANGLE_SIN) + val[idx].smoothed = + compassAngle(val[idx + 1].smoothed, val[idx + 2].smoothed); + if (val[idx].type == NMEA_BOAT_ANGLE_SIN) + val[idx].smoothed = boatAngle(val[idx + 1].smoothed, val[idx + 2].smoothed); + // some types just don't make sense to smooth -- use latest + if (val[idx].type == NMEA_BOAT_ANGLE) + val[idx].smoothed = val[idx].latest; + if (val[idx].type == NMEA_COMPASS_ANGLE) + val[idx].smoothed = val[idx].latest; + if (val[idx].type == NMEA_DDMM) + val[idx].smoothed = val[idx].latest; + if (val[idx].type == NMEA_HHMMSS) + val[idx].smoothed = val[idx].latest; + + val[idx].lastUpdate = millis(); // take a time stamp + if (val[idx].hist) { // there's a history struct for this tag + unsigned long seconds = (millis() - val[idx].hist->lastHistory) / 1000; + // do an update if the time has come, or if this is the first time through + if (seconds >= val[idx].hist->historyInterval || + val[idx].hist->lastHistory == 0) { + + // move the old history back in time by one step + for (unsigned i = 0; i < (val[idx].hist->n - 1); i++) + val[idx].hist->data[i] = val[idx].hist->data[i + 1]; + + // Create the new entry, scaling and offsetting the value to fit into an + // integer, and based on the smoothed value. + val[idx].hist->data[val[idx].hist->n - 1] = + val[idx].hist->scale * (val[idx].smoothed - val[idx].hist->offset); + val[idx].hist->lastHistory = millis(); + } + } +#endif // NMEA_EXTENSIONS +} + +/**************************************************************************/ +/*! + @brief Initialize the object. Build a val[] matrix of data values for + all of the enumerated values, including the extra values for the compound + angle types. The initializer shold probably leave it up to the user + sketch to decide which data values should carry the extra memory burden + of history. + @return none +*/ +/**************************************************************************/ +void Adafruit_GPS::data_init() { +#ifdef NMEA_EXTENSIONS + // fill all the data values with nothing + static char c[] = "NUL"; + for (int i = 0; i < (int)NMEA_MAX_INDEX; i++) { + initDataValue((nmea_index_t)i, c, NULL, NULL, 0, (nmea_value_type_t)0); + } + + // fill selected data values with the relevant information and pointers + static char BoatSpeedfmt[] = "%6.2f"; + static char WindSpeedfmt[] = "%6.1f"; + static char Speedunit[] = "knots"; + static char Anglefmt[] = "%6.0f"; + static char BoatAngleunit[] = "Degrees"; + static char TrueAngleunit[] = "Deg True"; + static char MagAngleunit[] = "Deg Mag"; + + static char HDOPlabel[] = "HDOP"; + initDataValue(NMEA_HDOP, HDOPlabel); + + static char LATlabel[] = "Lat"; + static char LATfmt[] = "%9.4f"; + static char LATunit[] = "DDD.dddd"; + initDataValue( + NMEA_LAT, LATlabel, LATfmt, LATunit, 0, + NMEA_BOAT_ANGLE); // angle from -180 to 180, or actually -90 to 90 for lat + + static char LONlabel[] = "Lon"; + initDataValue(NMEA_LON, LONlabel, LATfmt, LATunit, 0, + NMEA_BOAT_ANGLE); // angle from -180 to 180 + + static char LATWPlabel[] = "WP Lat"; + initDataValue(NMEA_LATWP, LATWPlabel, LATfmt, LATunit, 0, NMEA_BOAT_ANGLE); + + static char LONWPlabel[] = "WP Lon"; + initDataValue(NMEA_LONWP, LONWPlabel, LATfmt, LATunit, 0, NMEA_BOAT_ANGLE); + + static char SOGlabel[] = "SOG"; + initDataValue(NMEA_SOG, SOGlabel, BoatSpeedfmt, Speedunit); + + static char COGlabel[] = "COG"; + // types with sin/cos need two extra spots in the values matrix! + initDataValue(NMEA_COG, COGlabel, Anglefmt, TrueAngleunit, 0, + NMEA_COMPASS_ANGLE_SIN); // type: 0-360 angle with sin/cos 11 + + static char COGWPlabel[] = "WP COG"; + initDataValue(NMEA_COGWP, COGWPlabel, Anglefmt, TrueAngleunit, 0, + NMEA_COMPASS_ANGLE); // type: angle 0-360 1 + + static char XTElabel[] = "XTE"; + static char XTEfmt[] = "%6.2f"; + static char XTEunit[] = "NM"; + initDataValue(NMEA_XTE, XTElabel, XTEfmt, XTEunit); + + static char DISTWPlabel[] = "WP Dist"; + initDataValue(NMEA_DISTWP, DISTWPlabel, XTEfmt, XTEunit); + + static char AWAlabel[] = "AWA"; + initDataValue(NMEA_AWA, AWAlabel, Anglefmt, BoatAngleunit, 0, + NMEA_BOAT_ANGLE_SIN); // type: +-180 angle with sin/cos 12 + + static char AWSlabel[] = "AWS"; + initDataValue(NMEA_AWS, AWSlabel, WindSpeedfmt, Speedunit); + + static char TWAlabel[] = "TWA"; + initDataValue(NMEA_TWA, TWAlabel, Anglefmt, BoatAngleunit, 0, + NMEA_BOAT_ANGLE_SIN); // type: +-180 angle with sin/cos 12 + + static char TWDlabel[] = "TWD"; + initDataValue(NMEA_TWD, TWDlabel, Anglefmt, TrueAngleunit, 0, + NMEA_COMPASS_ANGLE_SIN); // type: 0-360 angle with sin/cos 11 + + static char TWSlabel[] = "TWS"; + initDataValue(NMEA_TWS, TWSlabel, WindSpeedfmt, Speedunit); + + static char VMGlabel[] = "VMG"; + initDataValue(NMEA_VMG, VMGlabel, BoatSpeedfmt, Speedunit); + + static char VMGWPlabel[] = "WP VMG"; + initDataValue(NMEA_VMGWP, VMGWPlabel, BoatSpeedfmt, Speedunit); + + static char HEELlabel[] = "Heel"; + static char HEELunit[] = "Deg Stbd"; + initDataValue(NMEA_HEEL, HEELlabel, Anglefmt, HEELunit, 0, + NMEA_BOAT_ANGLE); // type: angle +/-180 2 + + static char PITCHlabel[] = "Pitch"; + static char PITCHunit[] = "Deg Bow Up"; + initDataValue(NMEA_PITCH, PITCHlabel, Anglefmt, PITCHunit, 0, + NMEA_BOAT_ANGLE); // type: angle +/-180 2 + static char HDGlabel[] = "HDG"; + initDataValue(NMEA_HDG, HDGlabel, Anglefmt, MagAngleunit, 0, + NMEA_COMPASS_ANGLE_SIN); // type: 0-360 angle with sin/cos 11 + + static char HDTlabel[] = "HDG"; + initDataValue(NMEA_HDT, HDTlabel, Anglefmt, TrueAngleunit, 0, + NMEA_COMPASS_ANGLE_SIN); // type: 0-360 angle with sin/cos 11 + + static char VTWlabel[] = "VTW"; + initDataValue(NMEA_VTW, VTWlabel, BoatSpeedfmt, Speedunit); + + static char LOGlabel[] = "Log"; + static char LOGfmt[] = "%6.0f"; + static char LOGunit[] = "NM"; + initDataValue(NMEA_LOG, LOGlabel, LOGfmt, LOGunit); + + static char LOGRlabel[] = "Trip"; + static char LOGRfmt[] = "%6.2f"; + initDataValue(NMEA_LOG, LOGRlabel, LOGRfmt, LOGunit); + + static char DEPTHlabel[] = "Depth"; + static char DEPTHfmt[] = "%6.1f"; + static char DEPTHunit[] = "m"; + initDataValue(NMEA_DEPTH, DEPTHlabel, DEPTHfmt, DEPTHunit); + + static char RPM_M1label[] = "Motor 1"; + static char RPM_M1fmt[] = "%6.0f"; + static char RPM_M1unit[] = "RPM"; + initDataValue(NMEA_RPM_M1, RPM_M1label, RPM_M1fmt, RPM_M1unit); + + static char TEMPERATURE_M1label[] = "Temp 1"; + static char TEMPERATURE_M1fmt[] = "%6.0f"; + static char TEMPERATURE_M1unit[] = "Deg C"; + initDataValue(NMEA_TEMPERATURE_M1, TEMPERATURE_M1label, TEMPERATURE_M1fmt, + TEMPERATURE_M1unit); + + static char PRESSURE_M1label[] = "Oil 1"; + static char PRESSURE_M1fmt[] = "%6.0f"; + static char PRESSURE_M1unit[] = "kPa"; + initDataValue(NMEA_PRESSURE_M1, PRESSURE_M1label, PRESSURE_M1fmt, + PRESSURE_M1unit); + + static char VOLTAGE_M1label[] = "Motor 1"; + static char VOLTAGE_M1fmt[] = "%6.2f"; + static char VOLTAGE_M1unit[] = "Volts"; + initDataValue(NMEA_VOLTAGE_M1, VOLTAGE_M1label, VOLTAGE_M1fmt, + VOLTAGE_M1unit); + + static char CURRENT_M1label[] = "Motor 1"; + static char CURRENT_M1fmt[] = "%6.1f"; + static char CURRENT_M1unit[] = "Amps"; + initDataValue(NMEA_CURRENT_M1, CURRENT_M1label, CURRENT_M1fmt, + CURRENT_M1unit); + + static char RPM_M2label[] = "Motor 2"; + initDataValue(NMEA_RPM_M2, RPM_M2label, RPM_M1fmt, RPM_M1unit); + + static char TEMPERATURE_M2label[] = "Temp 2"; + initDataValue(NMEA_TEMPERATURE_M2, TEMPERATURE_M2label, TEMPERATURE_M1fmt, + TEMPERATURE_M1unit); + + static char PRESSURE_M2label[] = "Oil 2"; + initDataValue(NMEA_PRESSURE_M2, PRESSURE_M2label, PRESSURE_M1fmt, + PRESSURE_M1unit); + + static char VOLTAGE_M2label[] = "Motor 2"; + initDataValue(NMEA_VOLTAGE_M2, VOLTAGE_M2label, VOLTAGE_M1fmt, + VOLTAGE_M1unit); + + static char CURRENT_M2label[] = "Motor 2"; + initDataValue(NMEA_CURRENT_M2, CURRENT_M2label, CURRENT_M1fmt, + CURRENT_M1unit); + + static char TEMPERATURE_AIRlabel[] = "Air"; + static char TEMPERATURE_AIRfmt[] = "%6.1f"; + static char TEMPERATURE_AIRunit[] = "Deg C"; + initDataValue(NMEA_TEMPERATURE_AIR, TEMPERATURE_AIRlabel, TEMPERATURE_AIRfmt, + TEMPERATURE_AIRunit); + + static char TEMPERATURE_WATERlabel[] = "Water"; + static char TEMPERATURE_WATERfmt[] = "%6.1f"; + static char TEMPERATURE_WATERunit[] = "Deg C"; + initDataValue(NMEA_TEMPERATURE_WATER, TEMPERATURE_WATERlabel, + TEMPERATURE_WATERfmt, TEMPERATURE_WATERunit); + + static char HUMIDITYlabel[] = "Humidity"; + static char HUMIDITYfmt[] = "%6.0f"; + static char HUMIDITYunit[] = "% RH"; + initDataValue(NMEA_HUMIDITY, HUMIDITYlabel, HUMIDITYfmt, HUMIDITYunit); + + static char BAROMETERlabel[] = "Barometer"; + static char BAROMETERfmt[] = "%6.0f"; + static char BAROMETERunit[] = "Pa"; + initDataValue(NMEA_BAROMETER, BAROMETERlabel, BAROMETERfmt, BAROMETERunit); +#endif // NMEA_EXTENSIONS +} + +#ifdef NMEA_EXTENSIONS +/**************************************************************************/ +/*! + @brief Clearer approach to retrieving NMEA values by allowing calls that + look like nmea.get(NMEA_TWA) instead of val[NMEA_TWA].latest. + Use newDataValue() to set the values. + @param idx the NMEA value's index + @return the latest NMEA value +*/ +/**************************************************************************/ +nmea_float_t Adafruit_GPS::get(nmea_index_t idx) { + if (idx >= NMEA_MAX_INDEX || idx < NMEA_HDOP) + return 0.0; + return val[idx].latest; +} + +/**************************************************************************/ +/*! + @brief Clearer approach to retrieving NMEA values + @param idx the NMEA value's index + @return the latest NMEA value, smoothed +*/ +/**************************************************************************/ +nmea_float_t Adafruit_GPS::getSmoothed(nmea_index_t idx) { + if (idx >= NMEA_MAX_INDEX || idx < NMEA_HDOP) + return 0.0; + return val[idx].smoothed; +} + +/**************************************************************************/ +/*! + @brief Initialize the contents of a data value table entry + @param idx The data index for the value to be initialized + @param label Pointer to a label string that describes the value + @param fmt Pointer to a sprintf format to use for the value, e.g. "%6.2f" + @param unit Pointer to a string for the units, e.g. "Deg Mag" + @param response Time constant for smoothing in ms. The longer the time + constant, the more slowly the smoothed value will move towards a new value. + @param type The type of data contained in the value. simple float 0, + angle 0-360 1, angle +/-180 2, angle with history centered +/- around + the latest angle 3, lat/lon DDMM.mm 10, time HHMMSS 20. + @return none +*/ +/**************************************************************************/ +void Adafruit_GPS::initDataValue(nmea_index_t idx, char *label, char *fmt, + char *unit, unsigned long response, + nmea_value_type_t type) { + if (idx < NMEA_MAX_INDEX) { + if (label) + val[idx].label = label; + if (fmt) + val[idx].fmt = fmt; + if (unit) + val[idx].unit = unit; + if (response) + val[idx].response = response; + val[idx].type = type; + if ((int)(val[idx].type / 10) == + 1) { // angle with sin/cos component recording + initDataValue((nmea_index_t)( + idx + 1)); // initialize the next two data values as well + initDataValue((nmea_index_t)(idx + 2)); + } + } +} + +/**************************************************************************/ +/*! + @brief Attempt to add history to a data value table entry. If it fails + to malloc the space, history will not be added. Test the pointer for a + check if needed. + @param idx The data index for the value to have history recorded + @param scale Value for scaling the integer history list + @param offset Value for scaling the integer history list + @param historyInterval Approximate Time in seconds between historical + values. + @return pointer to the history +*/ +/**************************************************************************/ +nmea_history_t *Adafruit_GPS::initHistory(nmea_index_t idx, nmea_float_t scale, + nmea_float_t offset, + unsigned historyInterval, + unsigned historyN) { + historyN = max((unsigned)10, historyN); + if (idx < NMEA_MAX_INDEX) { + // remove any existing history + if (val[idx].hist != NULL) + removeHistory(idx); + // space for the struct + val[idx].hist = (nmea_history_t *)malloc(sizeof(nmea_history_t)); + if (val[idx].hist != NULL) { + // space for the data array of the appropriate size + val[idx].hist->data = (int16_t *)malloc(sizeof(int16_t) * historyN); + if (val[idx].hist->data != NULL) { + // initialize the data array + for (unsigned i = 0; i < historyN; i++) + val[idx].hist->data[i] = 0; + } else + free(val[idx].hist); + } + if (val[idx].hist != NULL) { + val[idx].hist->n = historyN; + if (scale > 0.0) + val[idx].hist->scale = scale; + val[idx].hist->offset = offset; + if (historyInterval > 0) + val[idx].hist->historyInterval = historyInterval; + } + return val[idx].hist; + } + return NULL; +} + +/**************************************************************************/ +/*! + @brief Remove history from a data value table entry, if it has been added. + @param idx The data index for the value to have history removed + @return none +*/ +/**************************************************************************/ +void Adafruit_GPS::removeHistory(nmea_index_t idx) { + if (idx < NMEA_MAX_INDEX) { + if (val[idx].hist == NULL) + return; + free(val[idx].hist->data); + free(val[idx].hist); + val[idx].hist = NULL; + } +} + +/**************************************************************************/ +/*! + @brief Print out the current state of a data value. Primarily useful as + a debugging aid. + @param idx The index for the data value + @param n The number of history values to include + @return none +*/ +/**************************************************************************/ +void Adafruit_GPS::showDataValue(nmea_index_t idx, int n) { + Serial.print("idx: "); + if (idx < 10) + Serial.print(" "); + Serial.print(idx); + Serial.print(", "); + Serial.print(val[idx].label); + Serial.print(", "); + Serial.print(val[idx].latest, 4); + Serial.print(", "); + Serial.print(val[idx].smoothed, 4); + Serial.print(", at "); + Serial.print(val[idx].lastUpdate); + Serial.print(" ms, tau = "); + Serial.print(val[idx].response); + Serial.print(" ms, type:"); + Serial.print(val[idx].type); + Serial.print(", ockam:"); + Serial.print(val[idx].ockam); + if (val[idx].hist) { + Serial.print("\n History at "); + Serial.print(val[idx].hist->historyInterval); + Serial.print(" second intervals: "); + Serial.print(val[idx].hist->data[val[idx].hist->n - 1]); + for (unsigned i = val[idx].hist->n - 2; + i >= max(val[idx].hist->n - n, (unsigned)0); + i--) { // most recent first + Serial.print(", "); + Serial.print(val[idx].hist->data[i]); + } + } + Serial.print("\n"); + if (idx == NMEA_LAT) { + Serial.print(" latitude (DDMM.mmmm): "); + Serial.print(latitude, 4); + Serial.print(", lat: "); + Serial.print(lat); + Serial.print(", latitudeDegrees: "); + Serial.print(latitudeDegrees, 8); + Serial.print(", latitude_fixed: "); + Serial.println(latitude_fixed); + } + if (idx == NMEA_LON) { + Serial.print(" longitude (DDMM.mmmm): "); + Serial.print(longitude, 4); + Serial.print(", lon: "); + Serial.print(lon); + Serial.print(", longitudeDegrees: "); + Serial.print(longitudeDegrees, 8); + Serial.print(", longitude_fixed: "); + Serial.println(longitude_fixed); + } +} + +/**************************************************************************/ +/*! + @brief Check if it is a compound angle + @param idx The index for the data value + @return true if a compound angle requiring 3 contiguos data values. +*/ +/**************************************************************************/ +bool Adafruit_GPS::isCompoundAngle(nmea_index_t idx) { + if ((int)(val[idx].type / 10) == 1) // angle with sin/cos component recording + return true; + return false; +} + +/**************************************************************************/ +/*! + @brief Estimate a direction in -180 to 180 degree range from the values + of the sine and cosine of the compound angle, which could be noisy. + @param s The sin of the angle + @param c The cosine of the angle + @return The angle in -180 to 180 degree range. +*/ +/**************************************************************************/ +nmea_float_t Adafruit_GPS::boatAngle(nmea_float_t s, nmea_float_t c) { + nmea_float_t sAng = + asin(s) * RAD_TO_DEG; // put the sin angle in -90 to 90 range + while (sAng < -90) + sAng += 180.; + while (sAng > 90) + sAng -= 180.; + nmea_float_t cAng = + acos(c) * RAD_TO_DEG; // put the cos angle in 0 to 180 range + while (cAng < 0) + cAng += 180.; + while (cAng > 180) + cAng -= 180.; + // Pick the most accurate representation and translate + if (cAng < 45) + return sAng; // Close hauled + else { + if (cAng > 135) { // Running + if (sAng > 0) + return 180 - sAng; // on starboard tack + else + return -180 - sAng; // on port tack + } else { // Reaching + if (sAng < 0) + return -cAng; // on port tack + else + return cAng; // on starboard tack + } + } + return 9999; // you can't get here, but there must be an explicit return +} + +/**************************************************************************/ +/*! + @brief Estimate a direction in 0 to 360 degree range from the values + of the sine and cosine of the compound angle, which could be noisy. + @param s The sin of the angle + @param c The cosine of the angle + @return The angle in 0 to 360 degree range. +*/ +/**************************************************************************/ +nmea_float_t Adafruit_GPS::compassAngle(nmea_float_t s, nmea_float_t c) { + nmea_float_t ang = boatAngle(s, c); + if (ang < 5000) { // if reasonable range + while (ang < 0) + ang += 360.; // round up + while (ang > 360) + ang -= 360.; // round down + } + return ang; +} +#endif // NMEA_EXTENSIONS diff --git a/src/NMEA_parse.cpp b/src/NMEA_parse.cpp index ed3a0bb..822fc57 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,300 @@ 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; +}