esp32_Adafruit_GPS/src/NMEA_data.cpp

571 lines
21 KiB
C++
Raw Normal View History

/**************************************************************************/
/*!
@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
2020-01-29 18:10:43 +00:00
nmea_float_t w =
2020-01-29 17:57:20 +00:00
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
2020-01-30 22:09:35 +00:00
check if needed. Select scale and offset values carefully so that
operations and results will fit inside 16 bit integer limits. For example
a scale of 1.0 and an offset of 100000.0 would be a good choice for
atmospheric pressure in Pa with values ranging ~ +/- 3500, while a scale
of 10.0 would be pushing the integer limits.
@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: ");
2020-01-29 18:10:43 +00:00
if (idx < 10)
2020-01-29 17:57:20 +00:00
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]);
2020-01-29 17:57:20 +00:00
for (unsigned i = val[idx].hist->n - 2;
2020-01-29 18:10:43 +00:00
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: ");
2020-01-29 17:57:20 +00:00
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: ");
2020-01-29 17:57:20 +00:00
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