1
0
mirror of https://bitbucket.org/librepilot/librepilot.git synced 2024-12-03 11:24:10 +01:00
LibrePilot/flight/OpenPilot/Modules/GPS/NMEA.c
stac ecac1fb79d nmea: parse GSV sentences and populate GPSSatellites object
This creates a new UAVObject called GPSSatellites to hold
information about which satellites the GPS receiver can see
and the quality of their signals.

NMEA GSV sentences are now parsed.  The full set of GSV data
may be split across multiple GSV sentences, each containing
info for at most 4 satellites.  Once an entire set of GSV
records has been collected, the GPSSatellites UAVObject is
updated.

git-svn-id: svn://svn.openpilot.org/OpenPilot/trunk@1453 ebee16cc-31ac-478f-84a7-5cbb03baadba
2010-08-29 01:46:14 +00:00

708 lines
19 KiB
C

#include "openpilot.h"
#include "pios.h"
#include "NMEA.h"
#include "gpsposition.h"
#include "gpstime.h"
#include "gpssatellites.h"
// Debugging
//#define GPSDEBUG
#ifdef GPSDEBUG
#define NMEA_DEBUG_PKT ///< define to enable debug of all NMEA messages
#define NMEA_DEBUG_GGA ///< define to enable debug of GGA messages
#define NMEA_DEBUG_VTG ///< define to enable debug of VTG messages
#define NMEA_DEBUG_RMC ///< define to enable debug of RMC messages
#define NMEA_DEBUG_GSA ///< define to enable debug of GSA messages
#define NMEA_DEBUG_GSV ///< define to enable debug of GSV messages
#define NMEA_DEBUG_ZDA ///< define to enable debug of ZDA messages
#endif
/* Utility functions */
static float NMEA_real_to_float (char * nmea_real);
static bool NMEA_latlon_to_fixed_point (int32_t * latlon, char * nmea_latlon);
/* NMEA sentence parsers */
struct nmea_parser {
const char * prefix;
bool (*handler)(GPSPositionData * GpsData, char * sentence);
};
static bool nmeaProcessGPGGA (GPSPositionData * GpsData, char * sentence);
static bool nmeaProcessGPRMC (GPSPositionData * GpsData, char * sentence);
static bool nmeaProcessGPVTG (GPSPositionData * GpsData, char * sentence);
static bool nmeaProcessGPGSA (GPSPositionData * GpsData, char * sentence);
static bool nmeaProcessGPZDA (GPSPositionData * GpsData, char * sentence);
static bool nmeaProcessGPGSV (GPSPositionData * GpsData, char * sentence);
static struct nmea_parser nmea_parsers[] = {
{
.prefix = "GPGGA",
.handler = nmeaProcessGPGGA,
},
{
.prefix = "GPVTG",
.handler = nmeaProcessGPVTG,
},
{
.prefix = "GPGSA",
.handler = nmeaProcessGPGSA,
},
{
.prefix = "GPRMC",
.handler = nmeaProcessGPRMC,
},
{
.prefix = "GPZDA",
.handler = nmeaProcessGPZDA,
},
{
.prefix = "GPGSV",
.handler = nmeaProcessGPGSV,
},
};
static struct nmea_parser * NMEA_find_parser_by_prefix (char * prefix)
{
if (!prefix) {
return (NULL);
}
for (uint8_t i = 0; i < NELEMENTS(nmea_parsers); i++) {
struct nmea_parser * parser = &nmea_parsers[i];
/* Use strcmp to check for exact equality over the entire prefix */
if (!strcmp(prefix, parser->prefix)) {
/* Found an appropriate parser */
return (parser);
}
}
/* No matching parser for this prefix */
return (NULL);
}
/**
* Computes NMEA sentence checksum
* \param[in] Buffer for parsed nmea sentence
* \return false checksum not valid
* \return true checksum valid
*/
bool NMEA_checksum (char * nmea_sentence)
{
uint8_t checksum_computed = 0;
uint8_t checksum_received;
while (*nmea_sentence != '\0' &&
*nmea_sentence != '*') {
checksum_computed ^= *nmea_sentence;
nmea_sentence++;
}
/* Make sure we're now pointing at the checksum */
if (*nmea_sentence == '\0') {
/* Buffer ran out before we found a checksum marker */
return false;
}
/* Load the checksum from the buffer */
checksum_received = strtol(nmea_sentence + 1, NULL, 16);
//PIOS_COM_SendFormattedStringNonBlocking(COM_DEBUG_USART,"$%d=%d\r\n",checksum_received,checksum_computed);
return (checksum_computed == checksum_received);
}
/**
* Parses a complete NMEA sentence and updates the GPSPosition UAVObject
* \param[in] An NMEA sentence with a valid checksum
* \return true if the sentence was successfully parsed
* \return false if any errors were encountered with the parsing
*/
bool NMEA_update_position (char * nmea_sentence)
{
char * sentence = nmea_sentence;
struct nmea_parser * parser;
char * prefix;
/* Find out what kind of NMEA packet we're dealing with */
prefix = strsep(&sentence, ",");
/* Check if we have a parser for this packet type */
parser = NMEA_find_parser_by_prefix (prefix);
if (!parser) {
/* Valid but unhandled packet type */
return false;
}
/* Found a matching parser for this packet type */
/* Reject empty (but valid) packets without parsing */
if (sentence[0] == ',' &&
sentence[1] == ',' &&
sentence[2] == ',') {
/* Nothing to parse, */
return false;
}
/* Parse the sentence and update the GpsData object */
GPSPositionData GpsData;
GPSPositionGet(&GpsData);
if (!parser->handler(&GpsData, sentence)) {
/* Parse failed for valid checksum. Do not update the UAVObject. */
return false;
}
GPSPositionSet(&GpsData);
/* Tell the caller what kind of packet we just parsed */
return true;
}
/**
* Parse an NMEA GPGGA sentence and update the given UAVObject
* \param[in] A pointer to a GPSPosition UAVObject to be updated.
* \param[in] An NMEA sentence with a valid checksum
*/
static bool nmeaProcessGPGGA(GPSPositionData * GpsData, char * sentence)
{
char * next = sentence;
char * tokens;
char * delimiter = ",*";
#ifdef NMEA_DEBUG_GGA
PIOS_COM_SendFormattedStringNonBlocking(COM_DEBUG_USART,"$%s\r\n",sentence);
#endif
// get UTC time [hhmmss.sss]
tokens = strsep(&next, delimiter);
//strcpy(GpsInfo.TimeOfFix,tokens);
// next field: latitude
// get latitude [DDMM.mmmmm]
tokens = strsep(&next, delimiter);
if (!NMEA_latlon_to_fixed_point(&GpsData->Latitude, tokens)) {
return false;
}
// next field: N/S indicator
// correct latitute for N/S
tokens = strsep(&next, delimiter);
if(tokens[0] == 'S') GpsData->Latitude = -GpsData->Latitude;
// next field: longitude
// get longitude [dddmm.mmmmm]
tokens = strsep(&next, delimiter);
if (!NMEA_latlon_to_fixed_point(&GpsData->Longitude, tokens)) {
return false;
}
// next field: E/W indicator
// correct longitude for E/W
tokens = strsep(&next, delimiter);
if(tokens[0] == 'W') GpsData->Longitude = -GpsData->Longitude;
// next field: position fix status
// position fix status
// 0 = Invalid, 1 = Valid SPS, 2 = Valid DGPS, 3 = Valid PPS
// check for good position fix
tokens = strsep(&next, delimiter);
//if((tokens[0] != '0') || (tokens[0] != 0))
// GpsData.Updates++;
// next field: satellites used
// get number of satellites used in GPS solution
tokens = strsep(&next, delimiter);
GpsData->Satellites=atoi(tokens);
// next field: HDOP (horizontal dilution of precision)
tokens = strsep(&next, delimiter);
// next field: altitude
// get altitude (in meters mm.m)
tokens = strsep(&next, delimiter);
GpsData->Altitude = NMEA_real_to_float (tokens);
// next field: altitude units, always 'M'
tokens = strsep(&next, delimiter);
// next field: geoid separation
tokens = strsep(&next, delimiter);
GpsData->GeoidSeparation = NMEA_real_to_float (tokens);
// next field: separation units
tokens = strsep(&next, delimiter);
// next field: DGPS age
tokens = strsep(&next, delimiter);
// next field: DGPS station ID
tokens = strsep(&next, delimiter);
// next field: checksum
tokens = strsep(&next, delimiter);
return true;
}
/**
* Parse an NMEA GPRMC sentence and update the given UAVObject
* \param[in] A pointer to a GPSPosition UAVObject to be updated.
* \param[in] An NMEA sentence with a valid checksum
*/
static bool nmeaProcessGPRMC (GPSPositionData * GpsData, char * sentence)
{
char * next = sentence;
char * tokens;
char * delimiter = ",*";
GPSTimeData gpst;
GPSTimeGet(&gpst);
#ifdef NMEA_DEBUG_RMC
PIOS_COM_SendFormattedStringNonBlocking(COM_DEBUG_USART,"$%s\r\n",sentence);
#endif
// get UTC time [hhmmss.sss]
tokens = strsep(&next, delimiter);
float hms = NMEA_real_to_float(tokens);
gpst.Second = (int) hms % 100;
gpst.Minute = (((int) hms - gpst.Second) / 100) % 100;
gpst.Hour = (int) hms / 10000;
// next field: Navigation receiver warning A = OK, V = warning
tokens = strsep(&next, delimiter);
// next field: latitude
// get latitude [ddmm.mmmmm]
tokens = strsep(&next, delimiter);
if (!NMEA_latlon_to_fixed_point(&GpsData->Latitude, tokens)) {
return false;
}
// next field: N/S indicator
// correct latitute for N/S
tokens = strsep(&next, delimiter);
if(tokens[0] == 'S') GpsData->Latitude = -GpsData->Latitude;
// next field: longitude
// get longitude [dddmm.mmmmm]
tokens = strsep(&next, delimiter);
if (!NMEA_latlon_to_fixed_point(&GpsData->Longitude, tokens)) {
return false;
}
// next field: E/W indicator
// correct latitute for E/W
tokens = strsep(&next, delimiter);
if(tokens[0] == 'W') GpsData->Longitude = -GpsData->Longitude;
// next field: speed (knots)
// get speed in knots
tokens = strsep(&next, delimiter);
GpsData->Groundspeed = NMEA_real_to_float (tokens);
GpsData->Groundspeed *= 0.51444;
// next field: True course
// get True course
tokens = strsep(&next, delimiter);
GpsData->Heading = NMEA_real_to_float (tokens);
// next field: Date of fix
// get Date of fix
tokens = strsep(&next, delimiter);
// TODO: Should really not use a float here to be safe
float date = NMEA_real_to_float(tokens);
gpst.Year = (int) date % 100;
gpst.Month = (((int) date - gpst.Year) / 100) % 100;
gpst.Day = (int) (date / 10000);
gpst.Year += 2000;
// next field: Magnetic variation
tokens = strsep(&next, delimiter);
// next field: E or W
tokens = strsep(&next, delimiter);
// next field: Mode: A=autonomous, D=differential, E=Estimated, N=not valid, S=Simulator
tokens = strsep(&next, delimiter);
// next field: checksum
tokens = strsep(&next, delimiter);
GPSTimeSet(&gpst);
return true;
}
/**
* Parse an NMEA GPVTG sentence and update the given UAVObject
* \param[in] A pointer to a GPSPosition UAVObject to be updated.
* \param[in] An NMEA sentence with a valid checksum
*/
static bool nmeaProcessGPVTG (GPSPositionData * GpsData, char * sentence)
{
char * next = sentence;
char * tokens;
char * delimiter = ",*";
#ifdef NMEA_DEBUG_VTG
PIOS_COM_SendFormattedStringNonBlocking(COM_DEBUG_USART,"$%s\r\n",sentence);
#endif
// get course (true north ref) in degrees [ddd.dd]
tokens = strsep(&next, delimiter);
// next field: 'T'
tokens = strsep(&next, delimiter);
// next field: course (magnetic north)
// get course (magnetic north ref) in degrees [ddd.dd]
tokens = strsep(&next, delimiter);
// next field: 'M'
tokens = strsep(&next, delimiter);
// next field: speed (knots)
// get speed in knots
tokens = strsep(&next, delimiter);
// next field: 'N'
tokens = strsep(&next, delimiter);
// next field: speed (km/h)
// get speed in km/h
tokens = strsep(&next, delimiter);
// next field: 'K'
tokens = strsep(&next, delimiter);
// next field: checksum
tokens = strsep(&next, delimiter);
return true;
}
/**
* Parse an NMEA GPZDA sentence and update the @ref GPSTime object
* \param[in] A pointer to a GPSPosition UAVObject to be updated (unused).
* \param[in] An NMEA sentence with a valid checksum
*/
static bool nmeaProcessGPZDA (GPSPositionData * GpsData, char * sentence)
{
char * next = sentence;
char * tokens;
char * delimiter = ",*";
#ifdef NMEA_DEBUG_VZDA
PIOS_COM_SendFormattedStringNonBlocking(COM_DEBUG_USART,"$%s\r\n",sentence);
#endif
GPSTimeData gpst;
GPSTimeGet(&gpst);
tokens = strsep(&next, delimiter);
float hms = NMEA_real_to_float(tokens);
gpst.Second = (int) hms % 100;
gpst.Minute = (((int) hms - gpst.Second) / 100) % 100;
gpst.Hour = (int) hms / 10000;
tokens = strsep(&next, delimiter);
gpst.Day = (uint8_t) NMEA_real_to_float(next);
tokens = strsep(&next, delimiter);
gpst.Month = (uint8_t) NMEA_real_to_float(next);
tokens = strsep(&next, delimiter);
gpst.Year = (uint16_t) NMEA_real_to_float(next);
GPSTimeSet(&gpst);
return true;
}
static GPSSatellitesData gsv_partial;
/* Bitmaps of which sentences we're looking for to allow us to handle out-of-order GSVs */
static uint8_t gsv_expected_mask;
static uint8_t gsv_processed_mask;
/* Error counters */
static uint16_t gsv_incomplete_error;
static uint16_t gsv_duplicate_error;
static bool nmeaProcessGPGSV (GPSPositionData * GpsData, char * sentence)
{
char * next = sentence;
char * tokens;
char * delimiter = ",";
#ifdef NMEA_DEBUG_GSV
PIOS_COM_SendFormattedStringNonBlocking(COM_DEBUG_USART,"$%s\r\n",sentence);
#endif
/* Drop the checksum */
char * tmp = sentence;
char * tmp_delim = "*";
next = strsep(&tmp, tmp_delim);
/* # of sentences in full GSV data set */
tokens = strsep(&next, delimiter);
uint8_t total_sentences = atoi(tokens);
if ((total_sentences < 1) ||
(total_sentences > 8)) {
return false;
}
/* Sentence number within the current GSV data set */
tokens = strsep(&next, delimiter);
uint8_t current_sentence = atoi(tokens);
if (current_sentence < 1) {
return false;
}
/* # of satellites currently in view */
tokens = strsep(&next, delimiter);
gsv_partial.SatsInView = atoi(tokens);
/* Find out if this is the first sentence in the GSV set */
if (current_sentence == 1) {
if (gsv_expected_mask != gsv_processed_mask) {
/* We are starting over when we haven't yet finished our previous GSV group */
gsv_incomplete_error++;
}
/* First GSV sentence in the sequence, reset our expected_mask */
gsv_expected_mask = (1 << total_sentences) - 1;
}
uint8_t current_sentence_id = (1 << (current_sentence - 1));
if (gsv_processed_mask & current_sentence_id) {
/* Duplicate sentence in this GSV set */
gsv_duplicate_error++;
} else {
/* Note that we've seen this sentence */
gsv_processed_mask |= current_sentence_id;
}
/* Make sure this sentence can fit in our GPSSatellites object */
if ((current_sentence * 4) <= NELEMENTS(gsv_partial.PRN)) {
/* Process 4 blocks of satellite info */
for (uint8_t i = 0; next && i < 4; i++) {
uint8_t sat_index = ((current_sentence - 1) * 4) + i;
/* PRN number */
tokens = strsep(&next, delimiter);
gsv_partial.PRN[sat_index] = atoi(tokens);
/* Elevation */
tokens = strsep(&next, delimiter);
gsv_partial.Elevation[sat_index] = NMEA_real_to_float(tokens);
/* Azimuth */
tokens = strsep(&next, delimiter);
gsv_partial.Azimuth[sat_index] = NMEA_real_to_float(tokens);
/* SNR */
tokens = strsep(&next, delimiter);
gsv_partial.SNR[sat_index] = atoi(tokens);
}
}
/* Find out if we're finished processing all GSV sentences in the set */
if ((gsv_expected_mask != 0) &&
(gsv_processed_mask == gsv_expected_mask)) {
/* GSV set has been fully processed. Update the GPSSatellites object. */
GPSSatellitesSet (&gsv_partial);
memset ((void *)&gsv_partial, 0, sizeof(gsv_partial));
gsv_expected_mask = 0;
gsv_processed_mask = 0;
}
return true;
}
/**
* Parse an NMEA GPGSA sentence and update the given UAVObject
* \param[in] A pointer to a GPSPosition UAVObject to be updated.
* \param[in] An NMEA sentence with a valid checksum
*/
static bool nmeaProcessGPGSA (GPSPositionData * GpsData, char * sentence)
{
char * next = sentence;
char * tokens;
char * delimiter = ",*";
#ifdef NMEA_DEBUG_GSA
PIOS_COM_SendFormattedStringNonBlocking(COM_DEBUG_USART,"$%s\r\n",sentence);
#endif
// next field: Mode
// Mode: M=Manual, forced to operate in 2D or 3D, A=Automatic, 3D/2D
tokens = strsep(&next, delimiter);
// next field: Mode
// Mode: 1=Fix not available, 2=2D, 3=3D
tokens = strsep(&next, delimiter);
switch (atoi(tokens)) {
case 1:
GpsData->Status = GPSPOSITION_STATUS_NOFIX;
break;
case 2:
GpsData->Status = GPSPOSITION_STATUS_FIX2D;
break;
case 3:
GpsData->Status = GPSPOSITION_STATUS_FIX3D;
break;
default:
/* Unhandled */
return false;
break;
}
// next field: 3-14 IDs of SVs used in position fix (null for unused fields)
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
tokens = strsep(&next, delimiter);
// next field: PDOP
tokens = strsep(&next, delimiter);
GpsData->PDOP = NMEA_real_to_float (tokens);
// next field: HDOP
tokens = strsep(&next, delimiter);
GpsData->HDOP = NMEA_real_to_float (tokens);
// next field: VDOP
tokens = strsep(&next, delimiter);
GpsData->VDOP = NMEA_real_to_float (tokens);
// next field: checksum
tokens = strsep(&next, delimiter);
return true;
}
/* Parse a number encoded in a string of the format:
* [-]NN.nnnnn
* into a signed whole part and an unsigned fractional part.
* The fract_units field indicates the units of the fractional part as
* 1 whole = 10^fract_units fract
*/
static bool NMEA_parse_real (int32_t * whole, uint32_t * fract, uint8_t * fract_units, char * field)
{
char * s = field;
char * field_w;
char * field_f;
PIOS_DEBUG_Assert (whole);
PIOS_DEBUG_Assert (fract);
PIOS_DEBUG_Assert (fract_units);
field_w = strsep(&s, ".");
field_f = s;
*whole = strtol (field_w, NULL, 10);
if (field_w) {
/* decimal was found so we may have a fractional part */
*fract = strtoul (field_f, NULL, 10);
*fract_units = strlen(field_f);
} else {
/* no decimal was found, fractional part is zero */
*fract = 0;
*fract_units = 0;
}
return true;
}
/*
* This function only exists to deal with a linking
* failure in the stdlib function strtof(). This
* implementation does not rely on the _sbrk() syscall
* like strtof() does.
*/
static float NMEA_real_to_float (char * nmea_real)
{
int32_t whole;
uint32_t fract;
uint8_t fract_units;
/* Sanity checks */
PIOS_DEBUG_Assert (nmea_real);
if (! NMEA_parse_real (&whole, &fract, &fract_units, nmea_real)) {
return false;
}
/* Convert to float */
return (((float)whole) + fract * pow(10, -fract_units));
}
/*
* Parse a field in the format:
* DD[D]MM.mmmm[mm]
* into a fixed-point representation in units of (degrees * 1e-7)
*/
static bool NMEA_latlon_to_fixed_point (int32_t * latlon, char * nmea_latlon)
{
int32_t num_DDDMM;
uint32_t num_m;
uint8_t units;
/* Sanity checks */
PIOS_DEBUG_Assert (nmea_latlon);
PIOS_DEBUG_Assert (latlon);
if (! NMEA_parse_real (&num_DDDMM, &num_m, &units, nmea_latlon)) {
return false;
}
/* scale up the mmmm[mm] field apropriately depending on # of digits */
switch (units) {
case 0:
/* no digits, value is zero so no scaling */
break;
case 1: /* m */
num_m *= 1e6; /* m000000 */
break;
case 2: /* mm */
num_m *= 1e5; /* mm00000 */
break;
case 3: /* mmm */
num_m *= 1e4; /* mmm0000 */
break;
case 4: /* mmmm */
num_m *= 1e3; /* mmmm000 */
break;
case 5: /* mmmmm */
num_m *= 1e2; /* mmmmm00 */
break;
case 6: /* mmmmmm */
num_m *= 1e1; /* mmmmmm0 */
break;
default:
/* unhandled format */
num_m = 0;
break;
}
*latlon = (num_DDDMM / 100) * 1e7; /* scale the whole degrees */
*latlon += (num_DDDMM % 100) * 1e7 / 60; /* add in the scaled decimal whole minutes */
*latlon += num_m / 60; /* add in the scaled decimal fractional minutes */
return true;
}