diff options
Diffstat (limited to 'driver_nmea.c')
-rw-r--r-- | driver_nmea.c | 936 |
1 files changed, 936 insertions, 0 deletions
diff --git a/driver_nmea.c b/driver_nmea.c new file mode 100644 index 00000000..a8a2b556 --- /dev/null +++ b/driver_nmea.c @@ -0,0 +1,936 @@ +/* $Id$ */ +#include <sys/types.h> +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <math.h> +#include <string.h> +#include <stdarg.h> +#include <ctype.h> +#include <time.h> + +#include "gpsd_config.h" +#include "gpsd.h" +#include "timebase.h" + +#ifdef MKT3301_ENABLE +extern gps_mask_t processMKT3301(int c UNUSED, char *field[], struct gps_device_t *session); +#endif /* MKT3301_ENABLE */ + +#ifdef NMEA_ENABLE +/************************************************************************** + * + * Parser helpers begin here + * + **************************************************************************/ + +static void do_lat_lon(char *field[], struct gps_data_t *out) +/* process a pair of latitude/longitude fields starting at field index BEGIN */ +{ + double lat, lon, d, m; + char str[20], *p; + int updated = 0; + + if (*(p = field[0]) != '\0') { + strncpy(str, p, 20); + (void)sscanf(p, "%lf", &lat); + m = 100.0 * modf(lat / 100.0, &d); + lat = d + m / 60.0; + p = field[1]; + if (*p == 'S') + lat = -lat; + if (out->fix.latitude != lat) + out->fix.latitude = lat; + updated++; + } + if (*(p = field[2]) != '\0') { + strncpy(str, p, 20); + (void)sscanf(p, "%lf", &lon); + m = 100.0 * modf(lon / 100.0, &d); + lon = d + m / 60.0; + + p = field[3]; + if (*p == 'W') + lon = -lon; + if (out->fix.longitude != lon) + out->fix.longitude = lon; + updated++; + } +} + +/************************************************************************** + * + * Scary timestamp fudging begins here + * + * Four sentences, GGA and GLL and RMC and ZDA, contain timestamps. + * GGA/GLL/RMC timestamps look like hhmmss.ss, with the trailing .ss part + * optional. RMC has a date field, in the format ddmmyy. ZDA has + * separate fields for day/month/year, with a 4-digit year. This + * means that for RMC we must supply a century and for GGA and GLL we + * must supply a century, year, and day. We get the missing data from + * a previous RMC or ZDA; century in RMC is supplied by a constant if + * there has been no previous ZDA. + * + **************************************************************************/ + +#define DD(s) ((int)((s)[0]-'0')*10+(int)((s)[1]-'0')) + +static void merge_ddmmyy(char *ddmmyy, struct gps_device_t *session) +/* sentence supplied ddmmyy, but no century part */ +{ + if (session->driver.nmea.date.tm_year == 0) + session->driver.nmea.date.tm_year = (CENTURY_BASE + DD(ddmmyy+4)) - 1900; + session->driver.nmea.date.tm_mon = DD(ddmmyy+2)-1; + session->driver.nmea.date.tm_mday = DD(ddmmyy); +} + +static void merge_hhmmss(char *hhmmss, struct gps_device_t *session) +/* update from a UTC time */ +{ + int old_hour = session->driver.nmea.date.tm_hour; + + session->driver.nmea.date.tm_hour = DD(hhmmss); + if (session->driver.nmea.date.tm_hour < old_hour) /* midnight wrap */ + session->driver.nmea.date.tm_mday++; + session->driver.nmea.date.tm_min = DD(hhmmss+2); + session->driver.nmea.date.tm_sec = DD(hhmmss+4); + session->driver.nmea.subseconds = atof(hhmmss+4) - session->driver.nmea.date.tm_sec; +} + +#undef DD + +/************************************************************************** + * + * Compare GPS timestamps for equality. Depends on the fact that the + * timestamp granularity of GPS is 1/100th of a second. Use this to avoid + * naive float comparisons. + * + **************************************************************************/ + +#define GPS_TIME_EQUAL(a, b) (fabs((a) - (b)) < 0.01) + +/************************************************************************** + * + * NMEA sentence handling begins here + * + **************************************************************************/ + +static gps_mask_t processGPRMC(int count, char *field[], struct gps_device_t *session) +/* Recommend Minimum Course Specific GPS/TRANSIT Data */ +{ + /* + RMC,225446.33,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E,A*68 + 1 225446.33 Time of fix 22:54:46 UTC + 2 A Status of Fix: A = Autonomous, valid; + D = Differential, valid; V = invalid + 3,4 4916.45,N Latitude 49 deg. 16.45 min North + 5,6 12311.12,W Longitude 123 deg. 11.12 min West + 7 000.5 Speed over ground, Knots + 8 054.7 Course Made Good, True north + 9 181194 Date of fix 18 November 1994 + 10,11 020.3,E Magnetic variation 20.3 deg East + 12 A FAA mode indicator (NMEA 2.3 and later) + A=autonomous, D=differential, E=Estimated, + N=not valid, S=Simulator, M=Manual input mode + *68 mandatory nmea_checksum + + * SiRF chipsets don't return either Mode Indicator or magnetic variation. + */ + gps_mask_t mask = 0; + + if (strcmp(field[2], "V")==0) { + /* copes with Magellan EC-10X, see below */ + if (session->gpsdata.status != STATUS_NO_FIX) { + session->gpsdata.status = STATUS_NO_FIX; + mask |= STATUS_SET; + } + if (session->gpsdata.fix.mode >= MODE_2D) { + session->gpsdata.fix.mode = MODE_NO_FIX; + mask |= MODE_SET; + } + /* set something nz, so it won't look like an unknown sentence */ + mask |= ONLINE_SET; + } else if (strcmp(field[2], "A")==0) { + if (count > 9) { + merge_hhmmss(field[1], session); + merge_ddmmyy(field[9], session); + mask |= TIME_SET; + session->gpsdata.fix.time = (double)mkgmtime(&session->driver.nmea.date)+session->driver.nmea.subseconds; + if (!GPS_TIME_EQUAL(session->gpsdata.sentence_time, session->gpsdata.fix.time)) { + mask |= CYCLE_START_SET; + gpsd_report(LOG_PROG, "GPRMC starts a reporting cycle.\n"); + } + session->gpsdata.sentence_time = session->gpsdata.fix.time; + } + do_lat_lon(&field[3], &session->gpsdata); + mask |= LATLON_SET; + session->gpsdata.fix.speed = atof(field[7]) * KNOTS_TO_MPS; + session->gpsdata.fix.track = atof(field[8]); + mask |= (TRACK_SET | SPEED_SET); + /* + * This copes with GPSes like the Magellan EC-10X that *only* emit + * GPRMC. In this case we set mode and status here so the client + * code that relies on them won't mistakenly believe it has never + * received a fix. + */ + if (session->gpsdata.status == STATUS_NO_FIX) { + session->gpsdata.status = STATUS_FIX; /* could be DGPS_FIX, we can't tell */ + mask |= STATUS_SET; + } + if (session->gpsdata.fix.mode < MODE_2D) { + session->gpsdata.fix.mode = MODE_2D; + mask |= MODE_SET; + } + } + + gpsd_report(LOG_PROG, "GPRMC sets mode %d\n", session->gpsdata.fix.mode); + return mask; +} + +static gps_mask_t processGPGLL(int count, char *field[], struct gps_device_t *session) +/* Geographic position - Latitude, Longitude */ +{ + /* Introduced in NMEA 3.0. + + $GPGLL,4916.45,N,12311.12,W,225444,A,A*5C + + 1,2: 4916.46,N Latitude 49 deg. 16.45 min. North + 3,4: 12311.12,W Longitude 123 deg. 11.12 min. West + 5: 225444 Fix taken at 22:54:44 UTC + 6: A Data valid + 7: A Autonomous mode + 8: *5C Mandatory NMEA checksum + + 1,2 Latitude, N (North) or S (South) + 3,4 Longitude, E (East) or W (West) + 5 UTC of position + 6 A=Active, V=Void + 7 Mode Indicator + A = Autonomous mode + D = Differential Mode + E = Estimated (dead-reckoning) mode + M = Manual Input Mode + S = Simulated Mode + N = Data Not Valid + + I found a note at <http://www.secoh.ru/windows/gps/nmfqexep.txt> + indicating that the Garmin 65 does not return time and status. + SiRF chipsets don't return the Mode Indicator. + This code copes gracefully with both quirks. + + Unless you care about the FAA indicator, this sentence supplies nothing + that GPRMC doesn't already. But at least one Garmin GPS -- the 48 + actually ships updates in GPLL that aren't redundant. + */ + char *status = field[7]; + gps_mask_t mask = ERROR_SET; + + if (strcmp(field[6], "A")==0 && (count < 8 || *status != 'N')) { + int newstatus = session->gpsdata.status; + + mask = 0; + merge_hhmmss(field[5], session); + if (session->driver.nmea.date.tm_year == 0) + gpsd_report(LOG_WARN, "can't use GGL time until after ZDA or RMC has supplied a year.\n"); + else { + mask = TIME_SET; + session->gpsdata.fix.time = (double)mkgmtime(&session->driver.nmea.date)+session->driver.nmea.subseconds; + if (!GPS_TIME_EQUAL(session->gpsdata.sentence_time, session->gpsdata.fix.time)) { + mask |= CYCLE_START_SET; + gpsd_report(LOG_PROG, "GPGLL starts a reporting cycle.\n"); + } + session->gpsdata.sentence_time = session->gpsdata.fix.time; + } + do_lat_lon(&field[1], &session->gpsdata); + mask |= LATLON_SET; + if (count >= 8 && *status == 'D') + newstatus = STATUS_DGPS_FIX; /* differential */ + else + newstatus = STATUS_FIX; + /* + * This is a bit dodgy. Technically we shouldn't set the mode + * bit until we see GSA. But it may be later in the cycle, + * some devices like the FV-18 don't send it by default, and + * elsewhere in the code we want to be able to test for the + * presence of a valid fix with mode > MODE_NO_FIX. + */ + if (session->gpsdata.fix.mode < MODE_2D) { + session->gpsdata.fix.mode = MODE_2D; + mask |= MODE_SET; + } + session->gpsdata.status = newstatus; + mask |= STATUS_SET; + gpsd_report(LOG_PROG, "GPGLL sets status %d\n", session->gpsdata.status); + } + + return mask; +} + +static gps_mask_t processGPGGA(int c UNUSED, char *field[], struct gps_device_t *session) +/* Global Positioning System Fix Data */ +{ + /* + GGA,123519,4807.038,N,01131.324,E,1,08,0.9,545.4,M,46.9,M, , *42 + 1 123519 Fix taken at 12:35:19 UTC + 2,3 4807.038,N Latitude 48 deg 07.038' N + 4,5 01131.324,E Longitude 11 deg 31.324' E + 6 1 Fix quality: 0 = invalid, 1 = GPS, 2 = DGPS, + 3=PPS (Precise Position Service), + 4=RTK (Real Time Kinematic) with fixed integers, + 5=Float RTK, 6=Estimated, 7=Manual, 8=Simulator + 7 08 Number of satellites being tracked + 8 0.9 Horizontal dilution of position + 9,10 545.4,M Altitude, Metres above mean sea level + 11,12 46.9,M Height of geoid (mean sea level) above WGS84 + ellipsoid, in Meters + (empty field) time in seconds since last DGPS update + (empty field) DGPS station ID number (0000-1023) + */ + gps_mask_t mask; + + session->gpsdata.status = atoi(field[6]); + mask = STATUS_SET; + if (session->gpsdata.status > STATUS_NO_FIX) { + char *altitude; + double oldfixtime = session->gpsdata.fix.time; + + merge_hhmmss(field[1], session); + if (session->driver.nmea.date.tm_year == 0) + gpsd_report(LOG_WARN, "can't use GGA time until after ZDA or RMC has supplied a year.\n"); + else { + mask |= TIME_SET; + session->gpsdata.fix.time = (double)mkgmtime(&session->driver.nmea.date)+session->driver.nmea.subseconds; + if (!GPS_TIME_EQUAL(session->gpsdata.sentence_time, session->gpsdata.fix.time)) { + mask |= CYCLE_START_SET; + gpsd_report(LOG_PROG, "GPGGA starts a reporting cycle.\n"); + } + session->gpsdata.sentence_time = session->gpsdata.fix.time; + } + do_lat_lon(&field[2], &session->gpsdata); + mask |= LATLON_SET; + session->gpsdata.satellites_used = atoi(field[7]); + altitude = field[9]; + /* + * SiRF chipsets up to version 2.2 report a null altitude field. + * See <http://www.sirf.com/Downloads/Technical/apnt0033.pdf>. + * If we see this, force mode to 2D at most. + */ + if (altitude[0] == '\0') { + if (session->gpsdata.fix.mode == MODE_3D) { + session->gpsdata.fix.mode = session->gpsdata.status ? MODE_2D : MODE_NO_FIX; + mask |= MODE_SET; + } + } else { + double oldaltitude = session->gpsdata.fix.altitude; + + session->gpsdata.fix.altitude = atof(altitude); + mask |= ALTITUDE_SET; + /* + * This is a bit dodgy. Technically we shouldn't set the mode + * bit until we see GSA. But it may be later in the cycle, + * some devices like the FV-18 don't send it by default, and + * elsewhere in the code we want to be able to test for the + * presence of a valid fix with mode > MODE_NO_FIX. + */ + if (session->gpsdata.fix.mode < MODE_3D) { + session->gpsdata.fix.mode = MODE_3D; + mask |= MODE_SET; + } + + /* + * Compute climb/sink in the simplest possible way. + * This substitutes for the climb report provided by + * SiRF and Garmin chips, which might have some smoothing + * going on. + */ + if (isnan(oldaltitude)!=0 || session->gpsdata.fix.time==oldfixtime) + session->gpsdata.fix.climb = 0; + else { + session->gpsdata.fix.climb = (session->gpsdata.fix.altitude-oldaltitude)/(session->gpsdata.fix.time-oldfixtime); + } + mask |= CLIMB_SET; + } + if (strlen(field[11]) > 0) { + session->gpsdata.separation = atof(field[11]); + } else { + session->gpsdata.separation = wgs84_separation(session->gpsdata.fix.latitude,session->gpsdata.fix.longitude); + } + } + gpsd_report(LOG_PROG, "GPGGA sets status %d and mode %d (%s)\n", session->gpsdata.status, session->gpsdata.fix.mode, ((mask&MODE_SET)!=0) ? "changed" : "unchanged"); + return mask; +} + +static gps_mask_t processGPGSA(int count, char *field[], struct gps_device_t *session) +/* GPS DOP and Active Satellites */ +{ + /* + eg1. $GPGSA,A,3,,,,,,16,18,,22,24,,,3.6,2.1,2.2*3C + eg2. $GPGSA,A,3,19,28,14,18,27,22,31,39,,,,,1.7,1.0,1.3*35 + 1 = Mode: + M=Manual, forced to operate in 2D or 3D + A=Automatic, 3D/2D + 2 = Mode: 1=Fix not available, 2=2D, 3=3D + 3-14 = PRNs of satellites used in position fix (null for unused fields) + 15 = PDOP + 16 = HDOP + 17 = VDOP + */ + gps_mask_t mask; + int i; + + /* + * One chipset called the i.Trek M3 issues GPGSA lines that look like + * this: "$GPGSA,A,1,,,,*32" when it has no fix. This is broken + * in at least two ways: it's got the wrong number of fields, and + * it claims to be a valid sentence (A flag) when it isn't. + * Alarmingly, it's possible this error may be generic to SiRFstarIII. + */ + if (count < 17) + return ONLINE_SET; + + session->gpsdata.fix.mode = atoi(field[2]); + /* + * The first arm of this conditional ignores dead-reckoning + * fixes from an Antaris chipset. which returns E in field 2 + * for a dead-reckoning estimate. Fix by Andreas Stricker. + */ + if (session->gpsdata.fix.mode == 0 && field[2][0] == 'E') + mask = 0; + else + mask = MODE_SET; + gpsd_report(LOG_PROG, "GPGSA sets mode %d\n", session->gpsdata.fix.mode); + session->gpsdata.pdop = atof(field[15]); + session->gpsdata.hdop = atof(field[16]); + session->gpsdata.vdop = atof(field[17]); + session->gpsdata.satellites_used = 0; + memset(session->gpsdata.used,0,sizeof(session->gpsdata.used)); + /* the magic 6 here counts the tag, two mode fields, and the DOP fields */ + for (i = 0; i < count - 6; i++) { + int prn = atoi(field[i+3]); + if (prn > 0) + session->gpsdata.used[session->gpsdata.satellites_used++] = prn; + } + mask |= USED_SET; + if (strlen(field[count-3]) > 0) + mask |= HDOP_SET; + if (strlen(field[count-2]) > 0) + mask |= VDOP_SET; + if (strlen(field[count-1]) > 0) + mask |= HDOP_SET; + return mask; +} + +static gps_mask_t processGPGSV(int count, char *field[], struct gps_device_t *session) +/* GPS Satellites in View */ +{ + /* + GSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75 + 2 Number of sentences for full data + 1 Sentence 1 of 2 + 08 Total number of satellites in view + 01 Satellite PRN number + 40 Elevation, degrees + 083 Azimuth, degrees + 46 Signal-to-noise ratio in decibels + <repeat for up to 4 satellites per sentence> + There my be up to three GSV sentences in a data packet + */ + int n, fldnum; + if (count <= 3) { + gpsd_zero_satellites(&session->gpsdata); + session->gpsdata.satellites = 0; + return ERROR_SET; + } + if (count % 4 != 0){ + gpsd_report(LOG_WARN, "malformed GPGSV - fieldcount %d %% 4 != 0\n", count); + gpsd_zero_satellites(&session->gpsdata); + session->gpsdata.satellites = 0; + return ERROR_SET; + } + + session->driver.nmea.await = atoi(field[1]); + if (sscanf(field[2], "%d", &session->driver.nmea.part) < 1) { + gpsd_zero_satellites(&session->gpsdata); + return ERROR_SET; + } else if (session->driver.nmea.part == 1) + gpsd_zero_satellites(&session->gpsdata); + + for (fldnum = 4; fldnum < count; ) { + if (session->gpsdata.satellites >= MAXCHANNELS) { + gpsd_report(LOG_ERROR, "internal error - too many satellites!\n"); + gpsd_zero_satellites(&session->gpsdata); + break; + } + session->gpsdata.PRN[session->gpsdata.satellites] = atoi(field[fldnum++]); + session->gpsdata.elevation[session->gpsdata.satellites] = atoi(field[fldnum++]); + session->gpsdata.azimuth[session->gpsdata.satellites] = atoi(field[fldnum++]); + session->gpsdata.ss[session->gpsdata.satellites] = atoi(field[fldnum++]); + /* + * Incrementing this unconditionally falls afoul of chipsets like + * the Motorola Oncore GT+ that emit empty fields at the end of the + * last sentence in a GPGSV set if the number of satellites is not + * a multiple of 4. + */ + if (session->gpsdata.PRN[session->gpsdata.satellites] != 0) + session->gpsdata.satellites++; + } + if (session->driver.nmea.part == session->driver.nmea.await && atoi(field[3]) != session->gpsdata.satellites) + gpsd_report(LOG_WARN, "GPGSV field 3 value of %d != actual count %d\n", + atoi(field[3]), session->gpsdata.satellites); + + /* not valid data until we've seen a complete set of parts */ + if (session->driver.nmea.part < session->driver.nmea.await) { + gpsd_report(LOG_PROG, "Partial satellite data (%d of %d).\n", session->driver.nmea.part, session->driver.nmea.await); + return ERROR_SET; + } + /* + * This sanity check catches an odd behavior of SiRFstarII receivers. + * When they can't see any satellites at all (like, inside a + * building) they sometimes cough up a hairball in the form of a + * GSV packet with all the azimuth entries 0 (but nonzero + * elevations). This behavior was observed under SiRF firmware + * revision 231.000.000_A2. + */ + for (n = 0; n < session->gpsdata.satellites; n++) + if (session->gpsdata.azimuth[n] != 0) + goto sane; + gpsd_report(LOG_WARN, "Satellite data no good (%d of %d).\n", session->driver.nmea.part, session->driver.nmea.await); + gpsd_zero_satellites(&session->gpsdata); + return ERROR_SET; + sane: + gpsd_report(LOG_PROG, "Satellite data OK (%d of %d).\n", session->driver.nmea.part, session->driver.nmea.await); + return SATELLITE_SET; + } + +static gps_mask_t processPGRME(int c UNUSED, char *field[], struct gps_device_t *session) +/* Garmin Estimated Position Error */ +{ + /* + $PGRME,15.0,M,45.0,M,25.0,M*22 + 1 = horizontal error estimate + 2 = units + 3 = vertical error estimate + 4 = units + 5 = spherical error estimate + 6 = units + * + * Garmin won't say, but the general belief is that these are 50% CEP. + * We follow the advice at <http://gpsinformation.net/main/errors.htm>. + * If this assumption changes here, it should also change in garmin.c + * where we scale error estimates from Garmin binary packets, and + * in libgpsd_core.c where we generate $PGRME. + */ + if ((strcmp(field[2], "M")!=0) || + (strcmp(field[4], "M")!=0) || + (strcmp(field[6], "M")!=0)){ + session->gpsdata.fix.eph = + session->gpsdata.fix.epv = + session->gpsdata.epe = 100; + return ERROR_SET; + } + + session->gpsdata.fix.eph = atof(field[1]) * (GPSD_CONFIDENCE/CEP50_SIGMA); + session->gpsdata.fix.epv = atof(field[3]) * (GPSD_CONFIDENCE/CEP50_SIGMA); + session->gpsdata.epe = atof(field[5]) * (GPSD_CONFIDENCE/CEP50_SIGMA); + + return HERR_SET | VERR_SET | PERR_SET; +} + +static gps_mask_t processGPZDA(int c UNUSED, char *field[], struct gps_device_t *session) +/* Time & Date */ +{ + gps_mask_t mask = TIME_SET; + /* + $GPZDA,160012.71,11,03,2004,-1,00*7D + 1) UTC time (hours, minutes, seconds, may have fractional subsecond) + 2) Day, 01 to 31 + 3) Month, 01 to 12 + 4) Year (4 digits) + 5) Local zone description, 00 to +- 13 hours + 6) Local zone minutes description, apply same sign as local hours + 7) Checksum + */ + merge_hhmmss(field[1], session); + session->driver.nmea.date.tm_year = atoi(field[4]) - 1900; + session->driver.nmea.date.tm_mon = atoi(field[3])-1; + session->driver.nmea.date.tm_mday = atoi(field[2]); + session->gpsdata.fix.time = (double)mkgmtime(&session->driver.nmea.date)+session->driver.nmea.subseconds; + if (!GPS_TIME_EQUAL(session->gpsdata.sentence_time, session->gpsdata.fix.time)) { + mask |= CYCLE_START_SET; + gpsd_report(LOG_PROG, "GPZDA starts a reporting cycle.\n"); + } + session->gpsdata.sentence_time = session->gpsdata. fix.time; + return mask; +} + +#ifdef TNT_ENABLE +static gps_mask_t processTNTHTM(int c UNUSED, char *field[], struct gps_device_t *session) +{ + /* + * Proprietary sentence for True North Technologies Magnetic Compass. + * This may also apply to some Honeywell units since they may have been + * designed by True North. + + HTM,x.x,a,x.x,a,x.x,a,x.x,x.x*hh<cr><lf> + Fields in order: + 1. True heading in degrees + 2. magnetometer status character: + C = magnetometer calibration alarm + L = low alarm + M = low warning + N = normal + O = high warning + P = high alarm + V = magnetometer voltage level alarm + 3. pitch angle + 4. pitch status character - see field 2 above + 5. roll angle + 6. roll status character - see field 2 above + 7. dip angle + 8. relative magnitude horizontal component of earth's magnetic field + *hh mandatory nmea_checksum + */ + gps_mask_t mask; + mask = ONLINE_SET; + + //gpsd_zero_satellites(&session->gpsdata); + + /* + * Heading maps to track. + * Pitch maps to climb. + * Roll maps to speed. + * Dip maps to altitude. + */ + session->gpsdata.fix.time = timestamp(); + session->gpsdata.fix.track = atof(field[1]); + session->gpsdata.headingStatus = *field[2]; + session->gpsdata.fix.climb = atof(field[3]); + session->gpsdata.pitchStatus = *field[4]; + session->gpsdata.fix.speed = atof(field[5]); + session->gpsdata.rollStatus = *field[6]; + session->gpsdata.fix.altitude = atof(field[7]); + session->gpsdata.horzField = atof(field[8]); + session->gpsdata.fix.mode = MODE_3D; + mask |= (STATUS_SET | MODE_SET | TRACK_SET | SPEED_SET | CLIMB_SET | ALTITUDE_SET); + session->gpsdata.status = STATUS_FIX; /* could be DGPS_FIX */ + + gpsd_report(LOG_RAW, "Heading %lf %c.\n", session->gpsdata.fix.track, session->gpsdata.headingStatus); + return mask; +} +#endif /* TNT_ENABLE */ + +#ifdef OCEANSERVER_ENABLE +static gps_mask_t processOHPR(int c UNUSED, char *field[], struct gps_device_t *session) +{ + /* + * Proprietary sentence for OceanServer Magnetic Compass. + + OHPR,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x*hh<cr><lf> + Fields in order: + 1. Azimuth + 2. Pitch Angle + 3. Roll Angle + 4. Temperature + 5. Depth (feet) + 6. Magnetic Vector Length + 7-9. 3 axis Magnetic Field readings x,y,z + 10. Acceleration Vector Length + 11-13. 3 axis Acceleration Readings x,y,z + 14. Reserved + 15-16. 2 axis Gyro Output, X,y + 17. Reserved + 18. Reserved + *hh mandatory nmea_checksum + */ + gps_mask_t mask; + mask = ONLINE_SET; + + //gpsd_zero_satellites(&session->gpsdata); + + /* + * Heading maps to track. + * Pitch maps to climb. + * Roll maps to speed. + * Depth maps to altitude. + */ + session->gpsdata.fix.time = timestamp(); + session->gpsdata.fix.track = atof(field[1]); + session->gpsdata.fix.climb = atof(field[2]); + session->gpsdata.fix.speed = atof(field[3]); + session->gpsdata.temperature = atof(field[4]); + session->gpsdata.fix.altitude = atof(field[5]); + session->gpsdata.magnetic_length = atof(field[6]); + session->gpsdata.magnetic_field_x = atof(field[7]); + session->gpsdata.magnetic_field_y = atof(field[8]); + session->gpsdata.magnetic_field_z = atof(field[9]); + session->gpsdata.acceleration_length = atof(field[10]); + session->gpsdata.acceleration_field_x = atof(field[11]); + session->gpsdata.acceleration_field_y = atof(field[12]); + session->gpsdata.acceleration_field_z = atof(field[13]); + session->gpsdata.gyro_output_x = atof(field[15]); + session->gpsdata.gyro_output_y = atof(field[16]); + session->gpsdata.fix.mode = MODE_3D; + mask |= (STATUS_SET | MODE_SET | TRACK_SET | SPEED_SET | CLIMB_SET | ALTITUDE_SET); + session->gpsdata.status = STATUS_FIX; /* could be DGPS_FIX */ + + gpsd_report(LOG_RAW, "Heading %lf.\n", session->gpsdata.fix.track); + return mask; +} +#endif /* OCEANSERVER_ENABLE */ + +#ifdef ASHTECH_ENABLE +static gps_mask_t processPASHR(int c UNUSED, char *field[], struct gps_device_t *session) +{ + gps_mask_t mask; + mask = ONLINE_SET; + + if (0 == strcmp("RID", field[1])){ /* Receiver ID */ + (void)snprintf(session->subtype, sizeof(session->subtype)-1, + "%s ver %s", field[2], field[3]); + return 0; + } else if (0 == strcmp("POS", field[1])){ /* 3D Position */ + mask |= MODE_SET | STATUS_SET | CYCLE_START_SET; + if (0 == strlen(field[2])){ + /* empty first field means no 3D fix is available */ + session->gpsdata.status = STATUS_NO_FIX; + session->gpsdata.fix.mode = MODE_NO_FIX; + return mask; + } + + /* if we make it this far, we at least have a 3D fix */ + session->gpsdata.fix.mode = MODE_3D; + if (1 == atoi(field[2])) + session->gpsdata.status = STATUS_DGPS_FIX; + else + session->gpsdata.status = STATUS_FIX; + + session->gpsdata.satellites_used = atoi(field[3]); + merge_hhmmss(field[4], session); + do_lat_lon(&field[5], &session->gpsdata); + session->gpsdata.fix.altitude = atof(field[9]); + session->gpsdata.fix.track = atof(field[11]); + session->gpsdata.fix.speed = atof(field[12]) / MPS_TO_KPH; + session->gpsdata.fix.climb = atof(field[13]); + session->gpsdata.pdop = atof(field[14]); + session->gpsdata.hdop = atof(field[15]); + session->gpsdata.vdop = atof(field[16]); + session->gpsdata.tdop = atof(field[17]); + mask |= (TIME_SET | LATLON_SET | ALTITUDE_SET); + mask |= (SPEED_SET | TRACK_SET | CLIMB_SET); + mask |= (PDOP_SET | HDOP_SET | VDOP_SET | TDOP_SET); + } else if (0 == strcmp("SAT", field[1])){ /* Satellite Status */ + int i, n, p, u; + n = session->gpsdata.satellites = atoi(field[2]); + u = 0; + for (i = 0; i < n; i++){ + session->gpsdata.PRN[i] = p = atoi(field[3+i*5+0]); + session->gpsdata.azimuth[i] = atoi(field[3+i*5+1]); + session->gpsdata.elevation[i] = atoi(field[3+i*5+2]); + session->gpsdata.ss[i] = atoi(field[3+i*5+3]); + if (field[3+i*5+4][0] == 'U') + session->gpsdata.used[u++] = p; + } + session->gpsdata.satellites_used = u; + mask |= SATELLITE_SET | USED_SET; + } + return mask; +} +#endif /* ASHTECH_ENABLE */ + +#ifdef __UNUSED__ +static short nmea_checksum(char *sentence, unsigned char *correct_sum) +/* is the checksum on the specified sentence good? */ +{ + unsigned char sum = '\0'; + char c, *p = sentence, csum[3]; + + while ((c = *p++) != '*' && c != '\0') + sum ^= c; + if (correct_sum) + *correct_sum = sum; + (void)snprintf(csum, sizeof(csum), "%02X", sum); + return(csum[0]==toupper(p[0])) && (csum[1]==toupper(p[1])); +} +#endif /* __ UNUSED__ */ + +/************************************************************************** + * + * Entry points begin here + * + **************************************************************************/ + +/*@ -mayaliasunique @*/ +gps_mask_t nmea_parse(char *sentence, struct gps_device_t *session) +/* parse an NMEA sentence, unpack it into a session structure */ +{ + typedef gps_mask_t (*nmea_decoder)(int count, char *f[], struct gps_device_t *session); + static struct { + char *name; + int nf; /* minimum number of fields required to parse */ + nmea_decoder decoder; + } nmea_phrase[] = { + /*@ -nullassign @*/ + {"RMC", 8, processGPRMC}, + {"GGA", 13, processGPGGA}, + {"GLL", 7, processGPGLL}, + {"GSA", 17, processGPGSA}, + {"GSV", 0, processGPGSV}, + {"VTG", 0, NULL}, /* ignore Velocity Track made Good */ + {"ZDA", 7, processGPZDA}, + {"PGRMC", 0, NULL}, /* ignore Garmin Sensor Config */ + {"PGRME", 7, processPGRME}, + {"PGRMI", 0, NULL}, /* ignore Garmin Sensor Init */ + {"PGRMO", 0, NULL}, /* ignore Garmin Sentence Enable */ +#ifdef TNT_ENABLE + {"PTNTHTM", 9, processTNTHTM}, +#endif /* TNT_ENABLE */ +#ifdef ASHTECH_ENABLE + {"PASHR", 3, processPASHR}, /* general handler for Ashtech */ +#endif /* ASHTECH_ENABLE */ +#ifdef OCEANSERVER_ENABLE + {"OHPR", 18, processOHPR}, +#endif /* OCEANSERVER_ENABLE */ + /*@ +nullassign @*/ + }; + + int count; + gps_mask_t retval = 0; + unsigned int i; + char *p, *s, *e; +#ifndef USE_OLD_SPLIT + volatile char *t; +#endif +#ifdef __UNUSED__ + unsigned char sum; + + if (!nmea_checksum(sentence+1, &sum)) { + gpsd_report(LOG_ERROR, "Bad NMEA checksum: '%s' should be %02X\n", + sentence, sum); + return 0; + } +#endif /* __ UNUSED__ */ + + /* + * We've had reports that on the Garmin GPS-10 the device sometimes + * (1:1000 or so) sends garbage packets that have a valid checksum + * but are like 2 successive NMEA packets merged together in one + * with some fields lost. Usually these are much longer than the + * legal limit for NMEA, so we can cope by just tossing out overlong + * packets. This may be a generic bug of all Garmin chipsets. + */ + if (strlen(sentence) > NMEA_MAX) { + gpsd_report(LOG_WARN, "Overlong packet rejected.\n"); + return ONLINE_SET; + } + +#ifdef BREAK_REGRESSIONS + /* trim trailing CR/LF */ + for (i = 0; i < strlen(sentence); i++) + if ((sentence[i] == '\r') || (sentence[i] == '\n')){ + sentence[i] = '\0'; + break; + } +#endif + /*@ -usedef @*//* splint 3.1.1 seems to have a bug here */ + /* make an editable copy of the sentence */ + strncpy((char *)session->driver.nmea.fieldcopy, sentence, NMEA_MAX); + /* discard the checksum part */ + for (p = (char *)session->driver.nmea.fieldcopy; (*p != '*') && (*p >= ' '); ) ++p; + if (*p == '*') + *p++ = ','; /* otherwise we drop the last field */ + *p = '\0'; + e = p; + /* split sentence copy on commas, filling the field array */ +#ifdef USE_OLD_SPLIT + for (count = 0, p = (char *)session->driver.nmea.fieldcopy; p != NULL && *p != '\0'; ++count, p = strchr(p, ',')) { + *p = '\0'; + session->driver.nmea.field[count] = ++p; + } +#else + count = 0; + t = p; /* end of sentence */ + p = (char *)session->driver.nmea.fieldcopy + 1; /* beginning of tag, 'G' not '$' */ + /* while there is a search string and we haven't run off the buffer... */ + while((p != NULL) && (p <= t)){ + session->driver.nmea.field[count] = p; /* we have a field. record it */ + /*@ -compdef @*/ + if ((p = strchr(p, ',')) != NULL){ /* search for the next delimiter */ + *p = '\0'; /* replace it with a NUL */ + count++; /* bump the counters and continue */ + p++; + } + /*@ +compdef @*/ + } +#endif + /* point remaining fields at empty string, just in case */ + for (i = (unsigned int)count; + i < (unsigned)(sizeof(session->driver.nmea.field)/sizeof(session->driver.nmea.field[0])); + i++) + session->driver.nmea.field[i] = e; + /* dispatch on field zero, the sentence tag */ + for (i = 0; i < (unsigned)(sizeof(nmea_phrase)/sizeof(nmea_phrase[0])); ++i) { + s = session->driver.nmea.field[0]; + if (strlen(nmea_phrase[i].name) == 3) + s += 2; /* skip talker ID */ + if (strcmp(nmea_phrase[i].name, s) == 0) { + if (nmea_phrase[i].decoder!=NULL && (count >= nmea_phrase[i].nf)) { + retval = (nmea_phrase[i].decoder)(count, session->driver.nmea.field, session); + strncpy(session->gpsdata.tag, nmea_phrase[i].name, MAXTAGLEN); + session->gpsdata.sentence_length = strlen(sentence); + } else + retval = ONLINE_SET; /* unknown sentence */ + break; + } + } +#ifdef MKT3301_ENABLE + if (strncmp("PMTK", session->driver.nmea.field[0], 4) == 0) /* general handler for MKT3301 vendor specifics */ + retval = processMKT3301(count, session->driver.nmea.field, session); +#endif /* MKT3301_ENABLE */ + /*@ +usedef @*/ + return retval; +} +/*@ +mayaliasunique @*/ +#endif /* NMEA_ENABLE */ + +void nmea_add_checksum(char *sentence) +/* add NMEA checksum to a possibly *-terminated sentence */ +{ + unsigned char sum = '\0'; + char c, *p = sentence; + + if (*p == '$') { + p++; + } else { + gpsd_report(LOG_ERROR, "Bad NMEA sentence: '%s'\n", sentence); + } + while ( ((c = *p) != '*') && (c != '\0')) { + sum ^= c; + p++; + } + *p++ = '*'; + (void)snprintf(p, 5, "%02X\r\n", (unsigned)sum); +} + +ssize_t nmea_write(struct gps_device_t *session, char *buf, size_t len UNUSED) +/* ship a command to the GPS, adding * and correct checksum */ +{ + (void)strlcpy(session->msgbuf, buf, sizeof(session->msgbuf)); + if (session->msgbuf[0] == '$') { + (void)strlcat(session->msgbuf, "*", sizeof(session->msgbuf)); + nmea_add_checksum(session->msgbuf); + } else + (void)strlcat(session->msgbuf, "\r\n", sizeof(session->msgbuf)); + session->msgbuflen = strlen(session->msgbuf); + return gpsd_write(session, session->msgbuf, session->msgbuflen); +} + +ssize_t nmea_send(struct gps_device_t *session, const char *fmt, ... ) +{ + char buf[BUFSIZ]; + va_list ap; + + va_start(ap, fmt) ; + (void)vsnprintf(buf, sizeof(buf)-5, fmt, ap); + va_end(ap); + return nmea_write(session, buf, strlen(buf)); +} |