/* * This file is Copyright (c) 2010-2018 by the GPSD project * SPDX-License-Identifier: BSD-2-clause */ /* for vsnprintf() FreeBSD wants __ISO_C_VISIBLE >= 1999 */ #define __ISO_C_VISIBLE 1999 #include /* for isdigit() */ #include #include #include #include #include #include #include #include "gpsd.h" #include "strfuncs.h" #ifdef NMEA0183_ENABLE /************************************************************************** * * Parser helpers begin here * **************************************************************************/ /* process a pair of latitude/longitude fields starting at field index BEGIN * The input fields look like this: * field[0]: 4404.1237962 * field[1]: N * field[2]: 12118.8472460 * field[3]: W * input format of lat/lon is NMEA style DDDMM.mmmmmmm * yes, 7 digits of precision from survey grade GPS * * return: 0 == OK, non zero is failure. */ static int do_lat_lon(char *field[], struct gps_fix_t *out) { double d, m; double lon; double lat; if ('\0' == field[0][0] || '\0' == field[1][0] || '\0' == field[2][0] || '\0' == field[3][0]) { return 1; } lat = safe_atof(field[0]); m = 100.0 * modf(lat / 100.0, &d); lat = d + m / 60.0; if ('S' == field[1][0]) lat = -lat; lon = safe_atof(field[2]); m = 100.0 * modf(lon / 100.0, &d); lon = d + m / 60.0; if ('W' == field[3][0]) lon = -lon; if (0 == isfinite(lat) || 0 == isfinite(lon)) { return 2; } out->latitude = lat; out->longitude = lon; return 0; } /* process an FAA mode character * return status as in session->gpsdata.status */ static int faa_mode(char mode) { int newstatus = STATUS_FIX; switch (mode) { case '\0': /* missing */ newstatus = STATUS_NO_FIX; break; case 'A': /* Autonomous */ default: newstatus = STATUS_FIX; break; case 'D': /* Differential */ newstatus = STATUS_DGPS_FIX; break; case 'E': /* Estimated dead reckoning */ newstatus = STATUS_DR; break; case 'F': /* Float RTK */ newstatus = STATUS_RTK_FLT; break; case 'N': /* Data Not Valid */ /* already handled, for paranoia sake also here */ newstatus = STATUS_NO_FIX; break; case 'P': /* Precise (NMEA 4+) */ newstatus = STATUS_DGPS_FIX; /* sort of DGPS */ break; case 'R': /* fixed RTK */ newstatus = STATUS_RTK_FIX; break; case 'S': /* simulator */ newstatus = STATUS_NO_FIX; /* or maybe MODE_FIX? */ break; } return newstatus; } /************************************************************************** * * 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, * or .sss, 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 from the daemon's context (initialized at * startup time) if there has been no previous ZDA. * **************************************************************************/ #define DD(s) ((int)((s)[0]-'0')*10+(int)((s)[1]-'0')) /* sentence supplied ddmmyy, but no century part * * return: 0 == OK, greater than zero on failure */ static int merge_ddmmyy(char *ddmmyy, struct gps_device_t *session) { int yy; int mon; int mday; int year; int i; if (NULL == ddmmyy) { return 1; } for (i = 0; i < 6; i++) { if (0 == isdigit(ddmmyy[i])) { /* catches NUL and non-digits */ /* Telit HE910 can set year to "-1" (1999 - 2000) */ gpsd_log(&session->context->errout, LOG_WARN, "merge_ddmmyy(%s), malformed date\n", ddmmyy); return 2; } } /* check for termination */ if ('\0' != ddmmyy[6]) { /* missing NUL */ gpsd_log(&session->context->errout, LOG_WARN, "merge_ddmmyy(%s), malformed date\n", ddmmyy); return 3; } /* should be no defects left to segfault DD() */ yy = DD(ddmmyy + 4); mon = DD(ddmmyy + 2); mday = DD(ddmmyy); /* check for century wrap */ if (session->nmea.date.tm_year % 100 == 99 && yy == 0) gpsd_century_update(session, session->context->century + 100); year = (session->context->century + yy); /* 32 bit systems will break in 2038. * Telix fails on GPS rollover to 2099, which 32 bit system * can not handle. So wrap at 2080. That way 64 bit systems * work until 2080, and 2099 gets reported as 1999. * since GPS epoch started in 1980, allows for old NMEA to work. */ if (2080 <= year) { year -= 100; } if ( (1 > mon ) || (12 < mon ) ) { gpsd_log(&session->context->errout, LOG_WARN, "merge_ddmmyy(%s), malformed month\n", ddmmyy); return 4; } else if ( (1 > mday ) || (31 < mday ) ) { gpsd_log(&session->context->errout, LOG_WARN, "merge_ddmmyy(%s), malformed day\n", ddmmyy); return 5; } else { gpsd_log(&session->context->errout, LOG_DATA, "merge_ddmmyy(%s) sets year %d\n", ddmmyy, year); session->nmea.date.tm_year = year - 1900; session->nmea.date.tm_mon = mon - 1; session->nmea.date.tm_mday = mday; } return 0; } /* update from a UTC time * * return: 0 == OK, greater than zero on failure */ static int merge_hhmmss(char *hhmmss, struct gps_device_t *session) { int old_hour = session->nmea.date.tm_hour; int i, sublen; if (NULL == hhmmss) { return 1; } for (i = 0; i < 6; i++) { if (0 == isdigit(hhmmss[i])) { /* catches NUL and non-digits */ gpsd_log(&session->context->errout, LOG_WARN, "merge_hhmmss(%s), malformed time\n", hhmmss); return 2; } } /* don't check for termination, might have fractional seconds */ session->nmea.date.tm_hour = DD(hhmmss); if (session->nmea.date.tm_hour < old_hour) /* midnight wrap */ session->nmea.date.tm_mday++; session->nmea.date.tm_min = DD(hhmmss + 2); session->nmea.date.tm_sec = DD(hhmmss + 4); if ('.' == hhmmss[6] && 0 != isdigit(hhmmss[7])) { i = atoi(hhmmss + 7); sublen = strlen(hhmmss + 7); session->nmea.subseconds = i / pow(10.0, sublen); } else { session->nmea.subseconds = 0.0; } return 0; } static void register_fractional_time(const char *tag, const char *fld, struct gps_device_t *session) { if (fld[0] != '\0') { session->nmea.last_frac_time = session->nmea.this_frac_time; session->nmea.this_frac_time = safe_atof(fld); session->nmea.latch_frac_time = true; gpsd_log(&session->context->errout, LOG_DATA, "%s: registers fractional time %.3f\n", tag, session->nmea.this_frac_time); } } /************************************************************************** * * 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. * * WARNING: many GPS now report time to .sss. 1/1000th. But so far * no GPS reports fixes faster than 20Hz. * **************************************************************************/ #define GPS_TIME_EQUAL(a, b) (fabs((a) - (b)) < 0.01) /************************************************************************** * * NMEA sentence handling begins here * **************************************************************************/ /* process xxVTG * $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K * $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K,A * * where: * 1,2 054.7,T True track made good (degrees) * 3,4 034.4,M Magnetic track made good * 5,6 005.5,N Ground speed, knots * 7,8 010.2,K Ground speed, Kilometers per hour * 9 A Mode Indicator (optional) * see faa_mode() for possible mode values * * see also: * http://www.catb.org/gpsd/NMEA.html#_vtg_track_made_good_and_ground_speed */ static gps_mask_t processVTG(int count, char *field[], struct gps_device_t *session) { gps_mask_t mask = ONLINE_SET; if( (field[1][0] == '\0') || (field[5][0] == '\0')){ return mask; } /* ignore empty/missing field, fix mode of last resort */ if ((count > 9) && ('\0' != field[9][0])) { switch (field[9][0]) { case 'A': /* Autonomous, 2D or 3D fix */ /* FALL THROUGH */ case 'D': /* Differential, 2D or 3D fix */ // MODE_SET here causes issues // mask |= MODE_SET; break; case 'E': /* Estimated, DR only */ /* FALL THROUGH */ case 'N': /* Not Valid */ // MODE_SET here causes issues // mask |= MODE_SET; // nothing to use here, leave return mask; default: /* Huh? */ break; } } // set true track session->newdata.track = safe_atof(field[1]); mask |= TRACK_SET; // set magnetic variation if (field[3][0] != '\0'){ // ignore empty fields session->newdata.magnetic_track = safe_atof(field[3]); mask |= MAGNETIC_TRACK_SET; } session->newdata.speed = safe_atof(field[5]) * KNOTS_TO_MPS; mask |= SPEED_SET; gpsd_log(&session->context->errout, LOG_DATA, "VTG: course(T)=%.2f, course(M)=%.2f, speed=%.2f", session->newdata.track, session->newdata.magnetic_track, session->newdata.speed); return mask; } /* Recommend Minimum Course Specific GPS/TRANSIT Data */ static gps_mask_t processRMC(int count, char *field[], struct gps_device_t *session) { /* * 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) * see faa_mode() for possible mode values * 13 V Nav Status (NMEA 4.1 and later) * A=autonomous, * D=differential, * E=Estimated, * M=Manual input mode * N=not valid, * S=Simulator, * V = Valid * *68 mandatory nmea_checksum * * SiRF chipsets don't return either Mode Indicator or magnetic variation. */ gps_mask_t mask = ONLINE_SET; char status = field[2][0]; int newstatus; switch (status) { default: /* missing */ /* FALLTHROUGH */ case 'V': /* Invalid */ session->gpsdata.status = STATUS_NO_FIX; session->newdata.mode = MODE_NO_FIX; mask |= STATUS_SET | MODE_SET; break; case 'D': /* Differential Fix */ /* FALLTHROUGH */ case 'A': /* Valid Fix */ /* * The MTK3301, Royaltek RGM-3800, and possibly other * devices deliver bogus time values when the navigation * warning bit is set. */ if ('\0' != field[1][0] && 9 < count && '\0' != field[9][0]) { if (0 == merge_hhmmss(field[1], session) && 0 == merge_ddmmyy(field[9], session)) { /* got a good data/time */ mask |= TIME_SET; register_fractional_time(field[0], field[1], session); } } /* else, no point to the time only case, no regressions with that */ if (0 == do_lat_lon(&field[3], &session->newdata)) { newstatus = STATUS_FIX; mask |= LATLON_SET; if (MODE_2D >= session->gpsdata.fix.mode) { /* we have at least a 2D fix */ /* might cause blinking */ session->newdata.mode = MODE_2D; mask |= MODE_SET; } } else { newstatus = STATUS_NO_FIX; session->newdata.mode = MODE_NO_FIX; mask |= MODE_SET; } if ('\0' != field[7][0]) { session->newdata.speed = safe_atof(field[7]) * KNOTS_TO_MPS; mask |= SPEED_SET; } if ('\0' != field[8][0]) { session->newdata.track = safe_atof(field[8]); mask |= TRACK_SET; } if (count >= 12) { newstatus = faa_mode(field[12][0]); } /* * 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 (3 < session->gpsdata.satellites_used) { /* 4 sats used means 3D */ session->newdata.mode = MODE_3D; mask |= MODE_SET; } else if (0 != isfinite(session->gpsdata.fix.altitude)) { /* we probably have at least a 3D fix */ /* this handles old GPS that do not report 3D */ session->newdata.mode = MODE_3D; mask |= MODE_SET; } session->gpsdata.status = newstatus; } gpsd_log(&session->context->errout, LOG_DATA, "RMC: ddmmyy=%s hhmmss=%s lat=%.2f lon=%.2f " "speed=%.2f track=%.2f mode=%d status=%d\n", field[9], field[1], session->newdata.latitude, session->newdata.longitude, session->newdata.speed, session->newdata.track, session->newdata.mode, session->gpsdata.status); return mask; } /* Geographic position - Latitude, Longitude */ static gps_mask_t processGLL(int count, char *field[], struct gps_device_t *session) { /* 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 = Invalid data * 7 Mode Indicator * See faa_mode() for possible mode values. * * I found a note at * 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 two (Garmin GPS 48 and * Magellan Triton 400) actually ship updates in GLL that aren't redundant. * */ char *status = field[7]; gps_mask_t mask = ONLINE_SET; if (field[5][0] != '\0') { if (0 == merge_hhmmss(field[5], session)) { register_fractional_time(field[0], field[5], session); if (session->nmea.date.tm_year == 0) gpsd_log(&session->context->errout, LOG_WARN, "can't use GLL time until after ZDA or RMC" " has supplied a year.\n"); else { mask = TIME_SET; } } } if ('\0' == field[6][0] || 'V' == field[6][0]) { /* Invalid */ session->gpsdata.status = STATUS_NO_FIX; session->newdata.mode = MODE_NO_FIX; mask |= STATUS_SET | MODE_SET; } else if ('A' == field[6][0] && (count < 8 || *status != 'N') && 0 == do_lat_lon(&field[1], &session->newdata)) { int newstatus; mask |= LATLON_SET; newstatus = STATUS_FIX; if (count >= 8) { newstatus = faa_mode(*status); } /* * This is a bit dodgy. Technically we shouldn't set the mode * bit until we see GSA, or similar. 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 (0 != isfinite(session->gpsdata.fix.altitude)) { session->newdata.mode = MODE_3D; mask |= MODE_SET; } else if (3 < session->gpsdata.satellites_used) { /* 4 sats used means 3D */ session->newdata.mode = MODE_3D; mask |= MODE_SET; } else if (MODE_2D > session->gpsdata.fix.mode || 0 == isfinite(session->oldfix.altitude)) { session->newdata.mode = MODE_2D; mask |= MODE_SET; } session->gpsdata.status = newstatus; } else { session->gpsdata.status = STATUS_NO_FIX; session->newdata.mode = MODE_NO_FIX; mask |= STATUS_SET | MODE_SET; } gpsd_log(&session->context->errout, LOG_DATA, "GLL: hhmmss=%s lat=%.2f lon=%.2f mode=%d status=%d\n", field[5], session->newdata.latitude, session->newdata.longitude, session->newdata.mode, session->gpsdata.status); return mask; } /* Geographic position - Latitude, Longitude, and more */ static gps_mask_t processGNS(int count UNUSED, char *field[], struct gps_device_t *session) { /* Introduced in NMEA 4.0? * * This mostly duplicates RMC, except for the multi GNSS mode * indicatore. * * Example. Ignore the line break. * $GPGNS,224749.00,3333.4268304,N,11153.3538273,W,D,19,0.6,406.110, * -26.294,6.0,0138,S,*6A * * 1: 224749.00 UTC HHMMSS.SS. 22:47:49.00 * 2: 3333.4268304 Latitude DDMM.MMMMM. 33 deg. 33.4268304 min * 3: N Latitude North * 4: 12311.12 Longitude 111 deg. 53.3538273 min * 5: W Longitude West * 6: D FAA mode indicator * see faa_mode() for possible mode values * May be one to four characters. * Char 1 = GPS * Char 2 = GLONASS * Char 3 = ? * Char 4 = ? * 7: 19 Number of Satellites used in solution * 8: 0.6 HDOP * 9: 406110 Altitude in meters * 10: -26.294 Geoid separation in meters * 11: 6.0 Age of differential corrections, in seconds * 12: 0138 Differential reference station ID * 13: S NMEA 4.1+ Navigation status * S = Safe * C = Caution * U = Unsafe * V = Not valid for navigation * 8: *6A Mandatory NMEA checksum * */ int newstatus; int satellites_used; gps_mask_t mask = ONLINE_SET; if (field[1][0] != '\0') { if (0 == merge_hhmmss(field[1], session)) { register_fractional_time(field[0], field[1], session); if (session->nmea.date.tm_year == 0) { gpsd_log(&session->context->errout, LOG_WARN, "can't use GNS time until after ZDA or RMC" " has supplied a year.\n"); } else { mask = TIME_SET; } } } /* FAA mode: not valid, ignore * Yes, in 2019 a GLONASS only fix may be valid, but not worth * the confusion */ if ('\0' == field[6][0] || /* FAA mode: missing */ 'N' == field[6][0]) { /* FAA mode: not valid */ session->newdata.mode = MODE_NO_FIX; mask |= MODE_SET; return mask; } /* navigation status, assume S=safe and C=caution are OK */ /* can be missing on valid fix */ if ('U' == field[13][0] || /* Unsafe */ 'V' == field[13][0]) { /* not valid */ return mask; } satellites_used = atoi(field[7]); if (0 == do_lat_lon(&field[2], &session->newdata)) { mask |= LATLON_SET; session->newdata.mode = MODE_2D; if ('\0' != field[9][0]) { session->newdata.altitude = safe_atof(field[9]); if (0 != isfinite(session->newdata.altitude)) { mask |= ALTITUDE_SET; if (3 < satellites_used) { /* more than 3 sats used means 3D */ session->newdata.mode = MODE_3D; } } } } else { session->newdata.mode = MODE_NO_FIX; mask |= MODE_SET; } if (field[8][0] != '\0') { session->gpsdata.dop.hdop = safe_atof(field[8]); } newstatus = faa_mode(field[6][0]); session->gpsdata.status = newstatus; mask |= MODE_SET; gpsd_log(&session->context->errout, LOG_DATA, "GNS: hhmmss=%s lat=%.2f lon=%.2f mode=%d status=%d\n", field[1], session->newdata.latitude, session->newdata.longitude, session->newdata.mode, session->gpsdata.status); return mask; } /* Global Positioning System Fix Data */ static gps_mask_t processGGA(int c UNUSED, char *field[], struct gps_device_t *session) { /* * 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, * u-blox may use 1 for Estimated * 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 in use * 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 * 13 33 time in seconds since last DGPS update * usually empty * 14 1023 DGPS station ID number (0000-1023) * usually empty * * Some GPS, like the SiRFstarV in NMEA mode, send both GPGSA and * GLGPSA with identical data. */ gps_mask_t mask = ONLINE_SET; int newstatus; char last_last_gga_talker = session->nmea.last_gga_talker; int fix; char *altitude = field[9]; int satellites_visible; session->nmea.last_gga_talker = field[0][1]; if (0 == strlen(field[6])) { /* no data is no data, assume no fix * the test/daemon/myguide-3100.log shows lat/lon/alt but * no status, and related RMC shows no fix. */ fix = -1; } else { fix = atoi(field[6]); } switch (fix) { case 0: /* no fix */ newstatus = STATUS_NO_FIX; break; case 1: /* could be 2D, 3D, GNSSDR */ newstatus = STATUS_FIX; break; case 2: /* differential */ newstatus = STATUS_DGPS_FIX; break; case 3: /* GPS PPS, fix valid, could be 2D, 3D, GNSSDR */ newstatus = STATUS_FIX; break; case 4: /* RTK integer */ newstatus = STATUS_RTK_FIX; break; case 5: /* RTK float */ newstatus = STATUS_RTK_FLT; break; case 6: /* dead reckoning, could be valid or invalid */ newstatus = STATUS_DR; break; case 7: /* manual input, surveyed */ newstatus = STATUS_TIME; break; case 8: /* simulated mode */ /* Garmin GPSMAP and Gecko sends an 8, but undocumented why */ newstatus = STATUS_SIM; break; default: newstatus = -1; break; } if (0 <= newstatus) { session->gpsdata.status = newstatus; mask = STATUS_SET; } /* * There are some receivers (the Trimble Placer 450 is an example) that * don't ship a GSA with mode 1 when they lose satellite lock. Instead * they just keep reporting GGA and GSA on subsequent cycles with the * timestamp not advancing and a bogus mode. * * On the assumption that GGA is only issued once per cycle we can * detect this here (it would be nicer to do it on GSA but GSA has * no timestamp). * * SiRFstarV breaks this assumption, sending GGA with different * talker IDs. */ if ('\0' != last_last_gga_talker && last_last_gga_talker != session->nmea.last_gga_talker) { /* skip the time check */ session->nmea.latch_mode = 0; } else { session->nmea.latch_mode = strncmp(field[1], session->nmea.last_gga_timestamp, sizeof(session->nmea.last_gga_timestamp))==0; } if (session->nmea.latch_mode) { session->gpsdata.status = STATUS_NO_FIX; session->newdata.mode = MODE_NO_FIX; gpsd_log(&session->context->errout, LOG_PROG, "xxGGA: latch mode\n"); } else (void)strlcpy(session->nmea.last_gga_timestamp, field[1], sizeof(session->nmea.last_gga_timestamp)); /* satellites_visible is used as an accumulator in xxGSV * so if we set it here we break xxGSV * Some GPS, like SiRFstarV NMEA, report per GNSS used * counts in GPGGA and GLGGA. * session->gpsdata.satellites_visible = atoi(field[7]); */ satellites_visible = atoi(field[7]); if (0 == merge_hhmmss(field[1], session)) { register_fractional_time(field[0], field[1], session); if (session->nmea.date.tm_year == 0) gpsd_log(&session->context->errout, LOG_WARN, "can't use GGA time until after ZDA or RMC" " has supplied a year.\n"); else { mask |= TIME_SET; } } if (0 == do_lat_lon(&field[2], &session->newdata)) { session->newdata.mode = MODE_2D; mask |= LATLON_SET; if ('\0' == field[11][0]) { session->gpsdata.separation = wgs84_separation(session->newdata.latitude, session->newdata.longitude); } else { session->gpsdata.separation = safe_atof(field[11]); } /* * SiRF chipsets up to version 2.2 report a null altitude field. * See . * If we see this, force mode to 2D at most. */ if ('\0' != altitude[0]) { session->newdata.altitude = safe_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. * * Use satellites_visible as double check on MODE_3D */ if (4 <= satellites_visible) { session->newdata.mode = MODE_3D; } } if (3 > satellites_visible) { session->newdata.mode = MODE_NO_FIX; } } else { session->newdata.mode = MODE_NO_FIX; } mask |= MODE_SET; if ('\0' != field[8][0]) { session->gpsdata.dop.hdop = safe_atof(field[8]); } gpsd_log(&session->context->errout, LOG_DATA, "GGA: hhmmss=%s lat=%.2f lon=%.2f alt=%.2f mode=%d status=%d\n", field[1], session->newdata.latitude, session->newdata.longitude, session->newdata.altitude, session->newdata.mode, session->gpsdata.status); return mask; } static gps_mask_t processGST(int count, char *field[], struct gps_device_t *session) /* GST - GPS Pseudorange Noise Statistics */ { /* * GST,hhmmss.ss,x,x,x,x,x,x,x,*hh * 1 UTC time of associated GGA fix * 2 Total RMS standard deviation of ranges inputs to the nav solution * 3 Standard deviation (meters) of semi-major axis of error ellipse * 4 Standard deviation (meters) of semi-minor axis of error ellipse * 5 Orientation of semi-major axis of error ellipse (true north degrees) * 6 Standard deviation (meters) of latitude error * 7 Standard deviation (meters) of longitude error * 8 Standard deviation (meters) of altitude error * 9 Checksum */ gps_mask_t mask = ONLINE_SET; if (count < 8) { return mask; } #define PARSE_FIELD(n) (*field[n]!='\0' ? safe_atof(field[n]) : NAN) /* note this is not full UTC, just HHMMSS.ss */ /* this is not the current time, * it references another GPA of the same stamp. So do not set * any time stamps with it */ session->gpsdata.gst.utctime = PARSE_FIELD(1); session->gpsdata.gst.rms_deviation = PARSE_FIELD(2); session->gpsdata.gst.smajor_deviation = PARSE_FIELD(3); session->gpsdata.gst.sminor_deviation = PARSE_FIELD(4); session->gpsdata.gst.smajor_orientation = PARSE_FIELD(5); session->gpsdata.gst.lat_err_deviation = PARSE_FIELD(6); session->gpsdata.gst.lon_err_deviation = PARSE_FIELD(7); session->gpsdata.gst.alt_err_deviation = PARSE_FIELD(8); #undef PARSE_FIELD /* add in the time of start of today */ /* since it is NOT current time, do not register_fractional_time() */ session->gpsdata.gst.utctime += mkgmtime(&session->nmea.date); gpsd_log(&session->context->errout, LOG_DATA, "GST: utc = %.3f, rms = %.2f, maj = %.2f, min = %.2f," " ori = %.2f, lat = %.2f, lon = %.2f, alt = %.2f\n", session->gpsdata.gst.utctime, session->gpsdata.gst.rms_deviation, session->gpsdata.gst.smajor_deviation, session->gpsdata.gst.sminor_deviation, session->gpsdata.gst.smajor_orientation, session->gpsdata.gst.lat_err_deviation, session->gpsdata.gst.lon_err_deviation, session->gpsdata.gst.alt_err_deviation); mask = GST_SET | ONLINE_SET; return mask; } /* convert NMEA sigid to ublox sigid */ static unsigned char nmea_sigid_to_ubx(unsigned char nmea_sigid) { unsigned char ubx_sigid = 0; switch (nmea_sigid) { default: /* FALLTHROUGH */ case 0: /* missing, assume GPS L1 */ ubx_sigid = 0; break; case 1: /* L1 */ ubx_sigid = 0; break; case 2: /* E5, could be 5 or 6. */ ubx_sigid = 5; break; case 3: /* B2 or L2, could be 2 or 3. */ ubx_sigid = 2; break; case 5: /* L2 */ ubx_sigid = 4; break; case 6: /* L2CL */ ubx_sigid = 3; break; case 7: /* E1, could be 0 or 1. */ ubx_sigid = 0; break; } return ubx_sigid; } /* Deal with range-mapping attempts to use IDs 1-32 by Beidou, etc. * * See struct satellite_t in gps.h for ubx and nmea gnssid and svid mappings * * char *talker -- NMEA talker string * int nmea_satnum -- NMEA (All ver) satellite number (kinda the PRN) * int nmea_gnssid -- NMEA 4.10 gnssid, if known, otherwise zero * unsigned char *ubx_gnssid -- returned u-blox gnssid * unsigned char *ubx_svid -- returned u-blox gnssid * * Return the NMEA 2.x to 4.0 extended PRN */ static int nmeaid_to_prn(char *talker, int nmea_satnum, int nmea_gnssid, unsigned char *ubx_gnssid, unsigned char *ubx_svid) { /* * According to https://github.com/mvglasow/satstat/wiki/NMEA-IDs * and u-blox documentation. * NMEA IDs can be roughly divided into the following ranges: * * 1..32: GPS * 33..64: Various SBAS systems (EGNOS, WAAS, SDCM, GAGAN, MSAS) * 65..96: GLONASS * 152..158: Various SBAS systems (EGNOS, WAAS, SDCM, GAGAN, MSAS) * 173..182: IMES * 193..197: QZSS (undocumented u-blox goes to 199) * 201..235: Beidou (not NMEA, not u-blox?) * 301..336: Galileo * 401..437: Beidou * null: GLONASS unused * * The issue is what to do when GPSes from these different systems * fight for IDs in the 1-32 range, as in this pair of Beidou sentences * * $BDGSV,2,1,07,01,00,000,45,02,13,089,35,03,00,000,37,04,00,000,42*6E * $BDGSV,2,2,07,05,27,090,,13,19,016,,11,07,147,*5E * * Because the PRNs are only used for generating a satellite * chart, mistakes here aren't dangerous. The code will record * and use multiple sats with the same ID in one skyview; in * effect, they're recorded by the order in which they occur * rather than by PRN. */ int nmea2_prn = nmea_satnum; *ubx_gnssid = 0; /* default to ubx_gnssid is GPS */ *ubx_svid = 0; /* default to unnknown ubx_svid */ if (1 > nmea_satnum) { /* uh, oh... */ nmea2_prn = 0; } else if (0 < nmea_gnssid) { /* this switch handles case where nmea_gnssid is known */ switch (nmea_gnssid) { default: /* x = IMES Not defined by NMEA 4.10 */ /* FALLTHROUGH */ case 0: /* none given, ignore */ nmea2_prn = 0; break; case 1: if (33 > nmea_satnum) { /* 1 = GPS 1-32 */ *ubx_gnssid = 0; *ubx_svid = nmea_satnum; } else if (65 > nmea_satnum) { /* 1 = SBAS 33-64 */ *ubx_gnssid = 1; *ubx_svid = 87 + nmea_satnum; } else if (152 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (158 > nmea_satnum) { /* 1 = SBAS 152-158 */ *ubx_gnssid = 1; *ubx_svid = nmea_satnum; } else if (193 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (200 > nmea_satnum) { /* 1 = QZSS 193-197 */ /* undocumented u-blox goes to 199 */ *ubx_gnssid = 3; *ubx_svid = nmea_satnum - 192; } else { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } break; case 2: /* 2 = GLONASS 65-96, nul */ *ubx_gnssid = 6; *ubx_svid = nmea_satnum; break; case 3: /* 3 = Galileo 1-36 */ *ubx_gnssid = 2; *ubx_svid = nmea_satnum; nmea2_prn = 300 + nmea_satnum; break; case 4: /* 4 - BeiDou 1-37 */ *ubx_gnssid = 3; *ubx_svid = nmea_satnum; nmea2_prn = 300 + nmea_satnum; break; } /* left with NMEA 2.x to NMEA 4.0 satnums * use talker ID to disambiguate */ } else if (32 >= nmea_satnum) { *ubx_svid = nmea_satnum; switch (talker[0]) { case 'G': if (talker[1] == 'A') { /* Galileo */ nmea2_prn = 300 + nmea_satnum; *ubx_gnssid = 2; } else if (talker[1] == 'B') { /* map Beidou IDs 1..37 to 401..437 */ *ubx_gnssid = 3; nmea2_prn = 400 + nmea_satnum; } else if (talker[1] == 'L') { /* GLONASS GL doesn't seem to do this, better safe than sorry */ nmea2_prn = 64 + nmea_satnum; *ubx_gnssid = 6; } else if (talker[1] == 'N') { /* all of them, but only GPS is 0 < PRN < 33 */ } else if (talker[1] == 'P') { /* GPS,SBAS,QZSS, but only GPS is 0 < PRN < 33 */ } /* else ?? */ break; case 'B': if (talker[1] == 'D') { /* map Beidou IDs */ nmea2_prn = 400 + nmea_satnum; *ubx_gnssid = 3; } /* else ?? */ break; case 'Q': if (talker[1] == 'Z') { /* QZSS */ nmea2_prn = 192 + nmea_satnum; *ubx_gnssid = 5; } /* else ? */ break; default: /* huh? */ break; } } else if (64 >= nmea_satnum) { // NMEA-ID (33..64) to SBAS PRN 120-151. /* SBAS */ *ubx_gnssid = 1; *ubx_svid = 87 + nmea_satnum; } else if (96 >= nmea_satnum) { /* GLONASS 65..96 */ *ubx_gnssid = 6; *ubx_svid = nmea_satnum - 64; } else if (120 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (158 >= nmea_satnum) { /* SBAS 120..158 */ *ubx_gnssid = 1; *ubx_svid = nmea_satnum; } else if (173 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (182 >= nmea_satnum) { /* IMES 173..182 */ *ubx_gnssid = 4; *ubx_svid = nmea_satnum - 172; } else if (193 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (197 >= nmea_satnum) { /* QZSS 193..197 */ /* undocumented u-blox goes to 199 */ *ubx_gnssid = 5; *ubx_svid = nmea_satnum - 192; } else if (201 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (237 >= nmea_satnum) { /* BeiDou, non-standard, some SiRF put BeiDou 201-237 */ /* $GBGSV,2,2,05,209,07,033,*62 */ *ubx_gnssid = 3; *ubx_svid = nmea_satnum - 200; nmea2_prn += 200; /* move up to 400 where NMEA 2.x wants it. */ } else if (301 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (356 >= nmea_satnum) { /* Galileo 301..356 */ *ubx_gnssid = 2; *ubx_svid = nmea_satnum - 300; } else if (401 > nmea_satnum) { /* Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } else if (437 >= nmea_satnum) { /* BeiDou */ *ubx_gnssid = 3; *ubx_svid = nmea_satnum - 400; } else { /* greater than 437 Huh? */ *ubx_gnssid = 0; *ubx_svid = 0; nmea2_prn = 0; } return nmea2_prn; } static gps_mask_t processGSA(int count, char *field[], struct gps_device_t *session) /* GPS DOP and Active Satellites */ { #define GSA_TALKER field[0][1] /* * 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 * NMEA 4.10: $GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B * 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 * 18 - NMEA 4.1+ GNSS System ID, u-blox extended * 1 = GPS L1C/A, L2CL, L2CM * 2 = GLONASS L1 OF, L2 OF * 3 = Galileo E1C, E1B, E5 bl, E5 bQ * 4 = BeiDou B1I D1, B1I D2, B2I D1, B2I D12 * * Not all documentation specifies the number of PRN fields, it * may be variable. Most doc that specifies says 12 PRNs. * * The Navior-24 CH-4701 outputs 30 fields, 24 PRNs! * GPGSA,A,3,27,23,13,07,25,,,,,,,,,,,,,,,,,,,,07.9,06.0,05.2 * * The Skytraq S2525F8-BD-RTK output both GPGSA and BDGSA in the * same cycle: * $GPGSA,A,3,23,31,22,16,03,07,,,,,,,1.8,1.1,1.4*3E * $BDGSA,A,3,214,,,,,,,,,,,,1.8,1.1,1.4*18 * These need to be combined like GPGSV and BDGSV * * Some GPS emit GNGSA. So far we have not seen a GPS emit GNGSA * and then another flavor of xxGSA * * Some Skytraq will emit all GPS in one GNGSA, Then follow with * another GNGSA with the BeiDou birds. * * SEANEXX, SiRFstarIV, and others also do it twice in one cycle: * $GNGSA,A,3,31,26,21,,,,,,,,,,3.77,2.55,2.77*1A * $GNGSA,A,3,75,86,87,,,,,,,,,,3.77,2.55,2.77*1C * seems like the first is GNSS and the second GLONASS * * u-blox 9 outputs one per GNSS on each cycle. Note the * extra last parameter which is NMEA gnssid: * $GNGSA,A,3,13,16,21,15,10,29,27,20,,,,,1.05,0.64,0.83,1*03 * $GNGSA,A,3,82,66,81,,,,,,,,,,1.05,0.64,0.83,2*0C * $GNGSA,A,3,07,12,33,,,,,,,,,,1.05,0.64,0.83,3*0A * $GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B * Also note the NMEA 4.0 GLONASS PRN (82) in an NMEA 4.1 * sentence. */ gps_mask_t mask = ONLINE_SET; char last_last_gsa_talker = session->nmea.last_gsa_talker; int nmea_gnssid = 0; int nmea_sigid = 0; int ubx_sigid = 0; /* * 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 (18 > count) { gpsd_log(&session->context->errout, LOG_DATA, "xxGSA: malformed, setting ONLINE_SET only.\n"); mask = ONLINE_SET; } else if (session->nmea.latch_mode) { /* last GGA had a non-advancing timestamp; don't trust this GSA */ mask = ONLINE_SET; gpsd_log(&session->context->errout, LOG_DATA, "xxGSA: non-advancing timestamp\n"); } else { int i; session->newdata.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 ('E' == field[2][0]) mask = ONLINE_SET; else mask = MODE_SET; gpsd_log(&session->context->errout, LOG_PROG, "xxGSA sets mode %d\n", session->newdata.mode); if (19 < count ) { gpsd_log(&session->context->errout, LOG_WARN, "xxGSA: count %d too long!\n", count); } else { /* Just ignore the last fields of the Navior CH-4701 */ if (field[15][0] != '\0') session->gpsdata.dop.pdop = safe_atof(field[15]); if (field[16][0] != '\0') session->gpsdata.dop.hdop = safe_atof(field[16]); if (field[17][0] != '\0') session->gpsdata.dop.vdop = safe_atof(field[17]); if (19 == count && '\0' != field[18][0]) { /* get the NMEA 4.10 sigid */ nmea_sigid = atoi(field[18]); /* FIXME: ubx_sigid not used yet */ ubx_sigid = nmea_sigid_to_ubx(nmea_sigid); } } /* * might have gone from GPGSA to GLGSA/BDGSA * or GNGSA to GNGSA * in which case accumulate */ if ( '\0' == session->nmea.last_gsa_talker || (GSA_TALKER == session->nmea.last_gsa_talker && 'N' != GSA_TALKER) ) { session->gpsdata.satellites_used = 0; memset(session->nmea.sats_used, 0, sizeof(session->nmea.sats_used)); gpsd_log(&session->context->errout, LOG_DATA, "xxGSA: clear sats_used\n"); } session->nmea.last_gsa_talker = GSA_TALKER; if ((session->nmea.last_gsa_talker == 'D') || (session->nmea.last_gsa_talker == 'B')) session->nmea.seen_bdgsa = true; else if (session->nmea.last_gsa_talker == 'L') session->nmea.seen_glgsa = true; else if (session->nmea.last_gsa_talker == 'N') session->nmea.seen_gngsa = true; else if (session->nmea.last_gsa_talker == 'A') session->nmea.seen_gagsa = true; /* the magic 6 here counts the tag, two mode fields, and DOP fields */ for (i = 0; i < count - 6; i++) { int prn; int nmea_satnum; unsigned char ubx_gnssid; /* UNUSED */ unsigned char ubx_svid; /* UNUSED */ /* skip empty fields, otherwise empty becomes prn=200 */ nmea_satnum = atoi(field[i + 3]); if (1 > nmea_satnum) { continue; } prn = nmeaid_to_prn(field[0], nmea_satnum, nmea_gnssid, &ubx_gnssid, &ubx_svid); #ifdef __UNUSED__ /* debug */ gpsd_log(&session->context->errout, LOG_ERROR, "%s nmeaid_to_prn: nmea_gnssid %d nmea_satnum %d ubx_gnssid %d " "ubx_svid %d nmea2_prn %d\n", field[0], nmea_gnssid, nmea_satnum, ubx_gnssid, ubx_svid, prn); gpsd_log(&session->context->errout, LOG_ERROR, "%s count %d\n", field[0], count); #endif /* __UNUSED__ */ if (prn > 0) { /* check first BEFORE over-writing memory */ if (MAXCHANNELS <= session->gpsdata.satellites_used) { /* This should never happen as xxGSA is limited to 12, * except for the Navior-24 CH-4701. * But it could happen with multiple GSA per cycle */ break; } session->nmea.sats_used[session->gpsdata.satellites_used++] = (unsigned short)prn; } } mask |= DOP_SET | USED_IS; gpsd_log(&session->context->errout, LOG_DATA, "xxGSA: mode=%d used=%d pdop=%.2f hdop=%.2f vdop=%.2f " "ubx_sigid %d\n", session->newdata.mode, session->gpsdata.satellites_used, session->gpsdata.dop.pdop, session->gpsdata.dop.hdop, session->gpsdata.dop.vdop, ubx_sigid); } /* assumes GLGSA or BDGSA, if present, is emitted directly * after the GPGSA*/ if ((session->nmea.seen_glgsa || session->nmea.seen_bdgsa || session->nmea.seen_gagsa) && GSA_TALKER == 'P') { mask = ONLINE_SET; /* first of two GNGSA */ /* if mode == 1 some GPS only output 1 GNGSA, so ship mode always */ } else if ( 'N' != last_last_gsa_talker && 'N' == GSA_TALKER) { mask = ONLINE_SET | MODE_SET; } /* cast for 32/64 compatibility */ gpsd_log(&session->context->errout, LOG_PROG, "xxGSA: mask %#llx\n", (long long unsigned)mask); return mask; #undef GSA_TALKER } /* xxGSV - GPS Satellites in View */ static gps_mask_t processGSV(int count, char *field[], struct gps_device_t *session) { #define GSV_TALKER field[0][1] /* * GSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75 * 1) 2 Number of sentences for full data * 2) 1 Sentence 1 of 2 * 3) 08 Total number of satellites in view * 4) 01 Satellite PRN number * 5) 40 Elevation, degrees * 6) 083 Azimuth, degrees * 7) 46 Signal-to-noise ratio in decibels * * ) NMEA 4.1 signalId * ) checksum * * NMEA 4.1+: * $GAGSV,3,1,09,02,00,179,,04,09,321,,07,11,134,11,11,10,227,,7*7F * after the satellite block, before the checksum, new field: * 7 NMEA Signal ID * 1 = GPS L1C/A, BeiDou B1I D1, BeiDou B1I D2, GLONASS L1 OF * 2 = Galileo E5 bl, E5 bQ * 3 = BeiDou B2I D1, B2I D2 * 5 = GPS L2 CM * 6 = GPS L2 CL * 7 = Galileo E1C, E1B * * Can occur with talker IDs: * BD (Beidou), * GA (Galileo), * GB (Beidou), * GL (GLONASS), * GN (GLONASS, any combination GNSS), * GP (GPS, SBAS, QZSS), * QZ (QZSS). * * As of April 2019: * no gpsd regressions have GNGSV * every xxGSV cycle starts with GPGSV * xxGSV cycles may be spread over several xxRMC cycles * * GL may be (incorrectly) used when GSVs are mixed containing * GLONASS, GN may be (incorrectly) used when GSVs contain GLONASS * only. Usage is inconsistent. * * In the GLONASS version sat IDs run from 65-96 (NMEA0183 * standardizes this). At least two GPSes, the BU-353 GLONASS and * the u-blox NEO-M8N, emit a GPGSV set followed by a GLGSV set. * We have also seen two GPSes, the Skytraq S2525F8-BD-RTK and a * SiRF-IV variant, that emit GPGSV followed by BDGSV. We need to * combine these. * * The following shows how the Skytraq S2525F8-BD-RTK output both * GPGSV and BDGSV in the same cycle: * $GPGSV,4,1,13,23,66,310,29,03,65,186,33,26,43,081,27,16,41,124,38*78 * $GPGSV,4,2,13,51,37,160,38,04,37,066,25,09,34,291,07,22,26,156,37*77 * $GPGSV,4,3,13,06,19,301,,31,17,052,20,193,11,307,,07,11,232,27*4F * $GPGSV,4,4,13,01,03,202,30*4A * $BDGSV,1,1,02,214,55,153,40,208,01,299,*67 * * The driver automatically adapts to either case, but it takes until the * second cycle (usually 10 seconds from device connect) for it to * learn to expect BSDGV or GLGSV. * * Some GPS (Garmin 17N) spread the xxGSV over several cycles. So * cycles, or cycle time, can not be used to determine start of * xxGSV cycle. * * NMEA 4.1 adds a signal-ID field just before the checksum. First * seen in May 2015 on a u-blox M8. It can output 2 sets of GPGSV * in one cycle, one for L1C and the other for L2C. */ int n, fldnum; unsigned char nmea_sigid = 0; int nmea_gnssid = 0; unsigned char ubx_sigid = 0; if (count <= 3) { gpsd_log(&session->context->errout, LOG_WARN, "malformed xxGSV - fieldcount %d <= 3\n", count); gpsd_zero_satellites(&session->gpsdata); return ONLINE_SET; } gpsd_log(&session->context->errout, LOG_PROG, "x%cGSV: part %s of %s, last_gsv_talker '%#x' " " last_gsv_sigid %u\n", GSV_TALKER, field[2], field[1], session->nmea.last_gsv_talker, session->nmea.last_gsv_sigid); /* * This check used to be !=0, but we have loosen it a little to let by * NMEA 4.1 GSVs with an extra signal-ID field at the end. */ switch (count % 4) { case 0: /* normal, pre-NMEA 4.10 */ break; case 1: /* NMEA 4.10, get the signal ID */ nmea_sigid = atoi(field[count - 1]); ubx_sigid = nmea_sigid_to_ubx(nmea_sigid); break; default: /* bad count */ gpsd_log(&session->context->errout, LOG_WARN, "malformed GPGSV - fieldcount %d %% 4 != 0\n", count); gpsd_zero_satellites(&session->gpsdata); return ONLINE_SET; } session->nmea.await = atoi(field[1]); if ((session->nmea.part = atoi(field[2])) < 1) { gpsd_log(&session->context->errout, LOG_WARN, "malformed GPGSV - bad part\n"); gpsd_zero_satellites(&session->gpsdata); return ONLINE_SET; } if (session->nmea.part == 1) { /* * might have gone from GPGSV to GLGSV/BDGSV/QZGSV, * in which case accumulate * * NMEA 4.1 might have gone from GPGVS,sigid=x to GPGSV,sigid=y * * session->nmea.last_gsv_talker is zero at cycle start */ if (session->nmea.last_gsv_talker == '\0' || ('P' == GSV_TALKER && 0 == ubx_sigid)) { gpsd_log(&session->context->errout, LOG_PROG, "x%cGSV: new part %d, last_gsv_talker '%#x', zeroing\n", GSV_TALKER, session->nmea.part, session->nmea.last_gsv_talker); gpsd_zero_satellites(&session->gpsdata); } session->nmea.last_gsv_talker = GSV_TALKER; session->nmea.last_gsv_sigid = ubx_sigid; /* UNUSED */ switch (GSV_TALKER) { case 'A': session->nmea.seen_gagsv = true; break; case 'B': /* FALLTHROUGH */ case 'D': session->nmea.seen_bdgsv = true; break; case 'L': session->nmea.seen_glgsv = true; break; case 'P': session->nmea.seen_gpgsv = true; break; case 'Z': session->nmea.seen_qzss = true; break; default: /* uh, what? */ break; } } for (fldnum = 4; fldnum < count / 4 * 4;) { struct satellite_t *sp; int nmea_svid; if (session->gpsdata.satellites_visible >= MAXCHANNELS) { gpsd_log(&session->context->errout, LOG_ERROR, "xxGSV: internal error - too many satellites [%d]!\n", session->gpsdata.satellites_visible); gpsd_zero_satellites(&session->gpsdata); break; } sp = &session->gpsdata.skyview[session->gpsdata.satellites_visible]; nmea_svid = atoi(field[fldnum++]); if (0 == nmea_svid) { /* skip bogus fields */ continue; } /* FIXME: this ignores possible NMEA 4.1 Signal ID hint */ sp->PRN = (short)nmeaid_to_prn(field[0], nmea_svid, nmea_gnssid, &sp->gnssid, &sp->svid); #ifdef __UNUSED__ /* debug */ gpsd_log(&session->context->errout, LOG_ERROR, "%s nmeaid_to_prn: nmea_gnssid %d nmea_satnum %d " "ubx_gnssid %d ubx_svid %d nmea2_prn %d\n", field[0], nmea_gnssid, nmea_svid, sp->gnssid, sp->svid, sp->PRN); #endif /* __UNUSED__ */ sp->elevation = (short)atoi(field[fldnum++]); sp->azimuth = (short)atoi(field[fldnum++]); sp->ss = (float)atoi(field[fldnum++]); sp->used = false; sp->sigid = ubx_sigid; /* sadly NMEA 4.1 does not tell us which sigid (L1, L2) is * used. So if the ss is zero, do not mark used */ if (0 < sp->PRN && 0 < sp->ss) { for (n = 0; n < MAXCHANNELS; n++) if (session->nmea.sats_used[n] == (unsigned short)sp->PRN) { sp->used = true; break; } } /* * 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. */ session->gpsdata.satellites_visible++; } #if __UNUSED /* debug code */ gpsd_log(&session->context->errout, LOG_ERROR, "x%cGSV: vis %d gagsv %d bdgsv %d glgsv %d qzss %d\n", GSV_TALKER, session->gpsdata.satellites_visible, session->nmea.seen_gagsv, session->nmea.seen_bdgsv, session->nmea.seen_glgsv, session->nmea.seen_qzss); #endif /* * Alas, we can't sanity check field counts when there are multiple sat * pictures, because the visible member counts *all* satellites - you * get a bad result on the second and later SV spans. Note, this code * assumes that if any of the special sat pics occur they come right * after a stock GPGSV one. * * FIXME: Add per-talker totals so we can do this check properly. */ if (!(session->nmea.seen_glgsv || session->nmea.seen_bdgsv || session->nmea.seen_qzss || session->nmea.seen_gagsv)) if (session->nmea.part == session->nmea.await && atoi(field[3]) != session->gpsdata.satellites_visible) gpsd_log(&session->context->errout, LOG_WARN, "GPGSV field 3 value of %d != actual count %d\n", atoi(field[3]), session->gpsdata.satellites_visible); /* not valid data until we've seen a complete set of parts */ if (session->nmea.part < session->nmea.await) { gpsd_log(&session->context->errout, LOG_PROG, "xxGSV: Partial satellite data (%d of %d).\n", session->nmea.part, session->nmea.await); return ONLINE_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_visible; n++) if (session->gpsdata.skyview[n].azimuth != 0) goto sane; gpsd_log(&session->context->errout, LOG_WARN, "xxGSV: Satellite data no good (%d of %d).\n", session->nmea.part, session->nmea.await); gpsd_zero_satellites(&session->gpsdata); return ONLINE_SET; sane: session->gpsdata.skyview_time = NAN; gpsd_log(&session->context->errout, LOG_DATA, "xxGSV: Satellite data OK (%d of %d).\n", session->nmea.part, session->nmea.await); /* assumes GLGSV or BDGSV group, if present, is emitted after the GPGSV */ if ((session->nmea.seen_glgsv || session->nmea.seen_bdgsv || session->nmea.seen_qzss || session->nmea.seen_gagsv) && GSV_TALKER == 'P') return ONLINE_SET; #if __UNUSED /* debug code */ gpsd_log(&session->context->errout, LOG_ERROR, "x%cGSV: set skyview_time %.2f frac_time %.2f\n", GSV_TALKER, session->gpsdata.skyview_time, session->nmea.this_frac_time); #endif /* clear computed DOPs so they get recomputed. */ /* FIXME: this kills GPS reported dops... */ session->gpsdata.dop.xdop = NAN; session->gpsdata.dop.ydop = NAN; session->gpsdata.dop.tdop = NAN; session->gpsdata.dop.gdop = NAN; return SATELLITE_SET; #undef GSV_TALKER } /* Garmin Estimated Position Error */ static gps_mask_t processPGRME(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $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 . * * 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. */ gps_mask_t mask = ONLINE_SET; if ('M' != field[2][0] || 'M' != field[4][0] || 'M' != field[6][0]) { mask = ONLINE_SET; } else { session->newdata.epx = session->newdata.epy = safe_atof(field[1]) * (1 / sqrt(2)) * (GPSD_CONFIDENCE / CEP50_SIGMA); session->newdata.epv = safe_atof(field[3]) * (GPSD_CONFIDENCE / CEP50_SIGMA); session->newdata.sep = safe_atof(field[5]) * (GPSD_CONFIDENCE / CEP50_SIGMA); mask = HERR_SET | VERR_SET | PERR_IS; } gpsd_log(&session->context->errout, LOG_DATA, "PGRME: epx=%.2f epy=%.2f sep=%.2f\n", session->newdata.epx, session->newdata.epy, session->newdata.sep); return mask; } /* Garmin GPS Fix Data Sentence * * FIXME: seems to happen after cycle ender, so little happens... */ static gps_mask_t processPGRMF(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $PGRMF,290,293895,160305,093802,13,5213.1439,N,02100.6511,E,A,2,0,226,2,1*11 * * 1 = GPS week * 2 = GPS seconds * 3 = UTC Date ddmmyy * 4 = UTC time hhmmss * 5 = GPS leap seconds * 6 = Latitude ddmm.mmmm * 7 = N or S * 8 = Longitude dddmm.mmmm * 9 = E or W * 10 = Mode, M = Manual, A = Automatic * 11 = Fix type, 0 = No fix, 2 = 2D fix, 2 = 3D fix * 12 = Ground Speed, 0 to 1151 km/hr * 13 = Course over ground, 0 to 359 degrees true * 14 = pdop, 0 to 9 * 15 = dop, 0 to 9 */ gps_mask_t mask = ONLINE_SET; if (0 == merge_hhmmss(field[4], session) && 0 == merge_ddmmyy(field[3], session)) { /* got a good data/time */ mask |= TIME_SET; } if ('A' != field[10][0]) { /* Huh? */ return mask; } if ('\0' != field[5][0]) { session->context->leap_seconds = atoi(field[5]); session->context->valid = LEAP_SECOND_VALID; } if (0 == do_lat_lon(&field[6], &session->newdata)) { mask |= LATLON_SET; } switch (field[11][0]) { default: /* Huh? */ break; case '0': session->newdata.mode = MODE_NO_FIX; mask |= MODE_SET; break; case '1': session->newdata.mode = MODE_2D; mask |= MODE_SET; break; case '2': session->newdata.mode = MODE_3D; mask |= MODE_SET; break; } session->newdata.speed = safe_atof(field[12]) / MPS_TO_KPH; session->newdata.track = safe_atof(field[13]); mask |= SPEED_SET | TRACK_SET; session->gpsdata.dop.pdop = safe_atof(field[14]); session->gpsdata.dop.tdop = safe_atof(field[15]); mask |= DOP_SET; gpsd_log(&session->context->errout, LOG_DATA, "PGRMF: pdop %.1f tdop %.1f \n", session->gpsdata.dop.pdop, session->gpsdata.dop.tdop); return mask; } /* Garmin Map Datum * * FIXME: seems to happen after cycle ender, so nothing happens... */ static gps_mask_t processPGRMM(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $PGRMM,NAD83*29 * 1 = Map Datum */ gps_mask_t mask = ONLINE_SET; if ('\0' != field[1][0]) { strlcpy(session->newdata.datum, field[1], sizeof(session->newdata.datum)); } gpsd_log(&session->context->errout, LOG_DATA, "PGRMM: datum=%.40s\n", session->newdata.datum); return mask; } /* Garmin Altitude Information */ static gps_mask_t processPGRMZ(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $PGRMZ,246,f,3*1B * 1 = Altitude * 2 = f (feet) * 3 = Mode * 1 = No Fix * 2 = 2D Fix * 3 = 3D Fix * * From: Garmin Proprietary NMEA 0183 Sentences * technical Specifications * 190-00684-00, Revision C December 2008 */ gps_mask_t mask = ONLINE_SET; if ('f' == field[2][0] && 0 < strlen(field[1])) { /* have a GPS altitude, must be 3D */ session->newdata.altitude = atoi(field[1]) / METERS_TO_FEET; mask |= (ALTITUDE_SET); } switch (field[3][0]) { default: /* Huh? */ break; case '1': session->newdata.mode = MODE_NO_FIX; mask |= MODE_SET; break; case '2': session->newdata.mode = MODE_2D; mask |= MODE_SET; break; case '3': session->newdata.mode = MODE_3D; mask |= MODE_SET; break; } gpsd_log(&session->context->errout, LOG_DATA, "PGRMZ: Altitude %.2f mode %d\n", session->newdata.altitude, session->newdata.mode); return mask; } /* Magellan Status */ static gps_mask_t processPMGNST(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $PMGNST,01.75,3,T,816,11.1,-00496,00*43 * 1 = Firmware version number * 2 = Mode (1 = no fix, 2 = 2D fix, 3 = 3D fix) * 3 = T if we have a fix * 4 = battery percentage left in tenths of a percent * 5 = time left on the GPS battery in hours * 6 = numbers change (freq. compensation?) * 7 = PRN number receiving current focus */ gps_mask_t mask = ONLINE_SET; int newmode = atoi(field[3]); if ('T' == field[4][0]) { switch(newmode) { default: session->newdata.mode = MODE_NO_FIX; break; case 2: session->newdata.mode = MODE_2D; break; case 3: session->newdata.mode = MODE_3D; break; } } else { /* Can report 3D fix, but 'F' for no fix */ session->newdata.mode = MODE_NO_FIX; } mask |= MODE_SET; gpsd_log(&session->context->errout, LOG_DATA, "PMGNST: mode: %d\n", session->newdata.mode); return mask; } /* SiRF Estimated Position Errors */ static gps_mask_t processPSRFEPE(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $PSRFEPE,100542.000,A,0.7,6.82,10.69,0.0,180.0*24 * 1 = UTC Time hhmmss.sss * 2 = Status. A = Valid, V = Data not valid * 3 = HDOP * 4 = EHPE meters (Estimated Horizontal Position Error) * 5 = EVPE meters (Estimated Vertical Position Error) * 6 = EHVE meters (Estimated Speed Over Ground/Velocity Error) * 7 = EHE degrees (Estimated Heading Error) * * SiRF won't say if these are 1-sigma or what... */ gps_mask_t mask = STATUS_SET; /* get time/ valid or not */ if ('\0' != field[1][0]) { if (0 == merge_hhmmss(field[1], session)) { register_fractional_time(field[0], field[1], session); if (session->nmea.date.tm_year == 0) { gpsd_log(&session->context->errout, LOG_WARN, "can't use PSRFEPE time until after ZDA or RMC" " has supplied a year.\n"); } else { mask |= TIME_SET; } } } if ('A' != field[2][0]) { /* Huh? */ return mask; } if ('\0' != field[3][0]) { /* This adds nothing, it just agrees with the gpsd calculation * from the skyview. Which is a nice confirmation. */ session->gpsdata.dop.hdop = safe_atof(field[3]); mask |= DOP_SET; } if ('\0' != field[4][0]) { /* EHPE (Estimated Horizontal Position Error) */ session->newdata.eph = safe_atof(field[4]); mask |= HERR_SET; } if ('\0' != field[5][0]) { /* Estimated Vertical Position Error (meters, 0.01 resolution) */ session->newdata.epv = safe_atof(field[5]); mask |= VERR_SET; } if ('\0' != field[6][0]) { /* Estimated Horizontal Speed Error meters/sec */ session->newdata.eps = safe_atof(field[6]); } if ('\0' != field[7][0]) { /* Estimated Heading Error degrees */ session->newdata.epd = safe_atof(field[7]); } gpsd_log(&session->context->errout, LOG_PROG, "PSRFEPE: hdop=%.1f eph=%.1f epv=%.1f eps=%.1f epd=%.1f\n", session->gpsdata.dop.hdop, session->newdata.eph, session->newdata.epv, session->newdata.eps, session->newdata.epd); return mask; } /* NMEA Map Datum * * FIXME: seems to happen after cycle ender, so nothing happens... */ static gps_mask_t processDTM(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $GPDTM,W84,C*52 * $GPDTM,xxx,x,xx.xxxx,x,xx.xxxx,x,,xxx*hh * 1 = Local datum code (xxx): * W84 – WGS84 * W72 – WGS72 * S85 – SGS85 * P90 – PE90 * 999 – User defined * IHO datum code * 2 = Local datum sub code (x) * 3 = Latitude offset in minutes (xx.xxxx) * 4 = Latitude offset mark (N: +, S: -) (x) * 5 = Longitude offset in minutes (xx.xxxx) * 6 = Longitude offset mark (E: +, W: -) (x) * 7 = Altitude offset in meters. Always null * 8 = Datum (xxx): * W84 – WGS84 * W72 – WGS72 * S85 – SGS85 * P90 – PE90 * 999 – User defined * IHO datum code * 9 = checksum */ int i; static struct { char *code; char *name; } codes[] = { {"W84", "WGS84"}, {"W72", "WGS72"}, {"S85", "SGS85"}, {"P90", "PE90"}, {"999", "User Defined"}, {"", ""}, }; gps_mask_t mask = ONLINE_SET; if ('\0' == field[1][0]) { return mask; } for (i = 0; ; i++) { if ('\0' == codes[i].code[0]) { /* not found */ strlcpy(session->newdata.datum, field[1], sizeof(session->newdata.datum)); break; } if (0 ==strcmp(codes[i].code, field[1])) { strlcpy(session->newdata.datum, codes[i].name, sizeof(session->newdata.datum)); break; } } gpsd_log(&session->context->errout, LOG_DATA, "xxDTM: datum=%.40s\n", session->newdata.datum); return mask; } /* NMEA 3.0 Estimated Position Error */ static gps_mask_t processGBS(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $GPGBS,082941.00,2.4,1.5,3.9,25,,-43.7,27.5*65 * 1) UTC time of the fix associated with this sentence (hhmmss.ss) * 2) Expected error in latitude (meters) * 3) Expected error in longitude (meters) * 4) Expected error in altitude (meters) * 5) PRN of most likely failed satellite * 6) Probability of missed detection for most likely failed satellite * 7) Estimate of bias in meters on most likely failed satellite * 8) Standard deviation of bias estimate * 9) NMEA 4.1 GNSS ID * 10) NMEA 4.1 Signal ID * Checksum * * Fields 2, 3 and 4 are one standard deviation. */ gps_mask_t mask = ONLINE_SET; /* register fractional time for end-of-cycle detection */ register_fractional_time(field[0], field[1], session); /* check that we're associated with the current fix */ if (session->nmea.date.tm_hour == DD(field[1]) && session->nmea.date.tm_min == DD(field[1] + 2) && session->nmea.date.tm_sec == DD(field[1] + 4)) { session->newdata.epy = safe_atof(field[2]); session->newdata.epx = safe_atof(field[3]); session->newdata.epv = safe_atof(field[4]); gpsd_log(&session->context->errout, LOG_DATA, "GBS: epx=%.2f epy=%.2f epv=%.2f\n", session->newdata.epx, session->newdata.epy, session->newdata.epv); mask = HERR_SET | VERR_SET; } else { gpsd_log(&session->context->errout, LOG_PROG, "second in $GPGBS error estimates doesn't match.\n"); } return mask; } static gps_mask_t processZDA(int c UNUSED, char *field[], struct gps_device_t *session) /* Time & Date */ { /* * $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 * * Note: some devices, like the u-blox ANTARIS 4h, are known to ship ZDAs * with some fields blank under poorly-understood circumstances (probably * when they don't have satellite lock yet). */ gps_mask_t mask = ONLINE_SET; int year, mon, mday, century; if (field[1][0] == '\0' || field[2][0] == '\0' || field[3][0] == '\0' || field[4][0] == '\0') { gpsd_log(&session->context->errout, LOG_WARN, "ZDA fields are empty\n"); return mask; } if (0 != merge_hhmmss(field[1], session)) { /* bad time */ return mask; } /* * We don't register fractional time here because want to leave * ZDA out of end-of-cycle detection. Some devices sensibly emit it only * when they have a fix, so watching for it can make them look * like they have a variable fix reporting cycle. */ year = atoi(field[4]); mon = atoi(field[3]); mday = atoi(field[2]); century = year - year % 100; if ( (1900 > year ) || (2200 < year ) ) { gpsd_log(&session->context->errout, LOG_WARN, "malformed ZDA year: %s\n", field[4]); } else if ( (1 > mon ) || (12 < mon ) ) { gpsd_log(&session->context->errout, LOG_WARN, "malformed ZDA month: %s\n", field[3]); } else if ( (1 > mday ) || (31 < mday ) ) { gpsd_log(&session->context->errout, LOG_WARN, "malformed ZDA day: %s\n", field[2]); } else { gpsd_century_update(session, century); session->nmea.date.tm_year = year - 1900; session->nmea.date.tm_mon = mon - 1; session->nmea.date.tm_mday = mday; mask = TIME_SET; } return mask; } static gps_mask_t processHDT(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $HEHDT,341.8,T*21 * * HDT,x.x*hh * * The only data field is true heading in degrees. * The following field is required to be 'T' indicating a true heading. * It is followed by a mandatory nmea_checksum. */ gps_mask_t mask = ONLINE_SET; double heading; if ( 0 == strlen(field[1])) { /* no data */ return mask; } heading = safe_atof(field[1]); if ((0.0 > heading) || (360.0 < heading)) { /* bad data */ return mask; } /* good data */ session->gpsdata.attitude.heading = heading; mask |= (ATTITUDE_SET); gpsd_log(&session->context->errout, LOG_RAW, "time %.3f, heading %lf.\n", session->newdata.time, session->gpsdata.attitude.heading); return mask; } static gps_mask_t processDBT(int c UNUSED, char *field[], struct gps_device_t *session) { /* * $SDDBT,7.7,f,2.3,M,1.3,F*05 * 1) Depth below sounder in feet * 2) Fixed value 'f' indicating feet * 3) Depth below sounder in meters * 4) Fixed value 'M' indicating meters * 5) Depth below sounder in fathoms * 6) Fixed value 'F' indicating fathoms * 7) Checksum. * * In real-world sensors, sometimes not all three conversions are reported. */ gps_mask_t mask = ONLINE_SET; /* this is criminal. Depth != altitude */ if (field[3][0] != '\0') { session->newdata.altitude = -safe_atof(field[3]); mask |= (ALTITUDE_SET); } else if (field[1][0] != '\0') { session->newdata.altitude = -safe_atof(field[1]) / METERS_TO_FEET; mask |= (ALTITUDE_SET); } else if (field[5][0] != '\0') { session->newdata.altitude = -safe_atof(field[5]) / METERS_TO_FATHOMS; mask |= (ALTITUDE_SET); } if ((mask & ALTITUDE_SET) != 0) { if (session->newdata.mode < MODE_3D) { session->newdata.mode = MODE_3D; mask |= MODE_SET; } } /* * Hack: We report depth below keep as negative altitude because there's * no better place to put it. Should work in practice as nobody is * likely to be operating a depth sounder at varying altitudes. */ gpsd_log(&session->context->errout, LOG_RAW, "mode %d, depth %lf.\n", session->newdata.mode, session->newdata.altitude); return mask; } /* GPS Text message */ static gps_mask_t processTXT(int count, char *field[], struct gps_device_t *session) { /* * $GNTXT,01,01,01,PGRM inv format*2A * 1 Number of sentences for full data * 1 Sentence 1 of 1 * 01 Message type * 00 - error * 01 - warning * 02 - notice * 07 - user * PGRM inv format ASCII text * * Can occur with talker IDs: * BD (Beidou), * GA (Galileo), * GB (Beidou), * GL (GLONASS), * GN (GLONASS, any combination GNSS), * GP (GPS, SBAS, QZSS), * QZ (QZSS). */ gps_mask_t mask = ONLINE_SET; int msgType = 0; char *msgType_txt = "Unknown"; if ( 5 != count) { return mask; } msgType = atoi(field[3]); switch ( msgType ) { case 0: msgType_txt = "Error"; break; case 1: msgType_txt = "Warning"; break; case 2: msgType_txt = "Notice"; break; case 7: msgType_txt = "User"; break; } /* maximum text lenght unknown, guess 80 */ gpsd_log(&session->context->errout, LOG_WARN, "TXT: %.10s: %.80s\n", msgType_txt, field[4]); 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. $PTNTHTM,14223,N,169,N,-43,N,13641,2454*15 HTM,x.x,a,x.x,a,x.x,a,x.x,x.x*hh Fields in order: 1. True heading (compass measurement + deviation + variation) 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 By default, angles are reported as 26-bit integers: weirdly, the technical manual says either 0 to 65535 or -32768 to 32767 can occur as a range. */ gps_mask_t mask = ONLINE_SET; session->gpsdata.attitude.heading = safe_atof(field[1]); session->gpsdata.attitude.mag_st = *field[2]; session->gpsdata.attitude.pitch = safe_atof(field[3]); session->gpsdata.attitude.pitch_st = *field[4]; session->gpsdata.attitude.roll = safe_atof(field[5]); session->gpsdata.attitude.roll_st = *field[6]; session->gpsdata.attitude.dip = safe_atof(field[7]); session->gpsdata.attitude.mag_x = safe_atof(field[8]); mask |= (ATTITUDE_SET); gpsd_log(&session->context->errout, LOG_RAW, "time %.3f, heading %lf (%c).\n", session->newdata.time, session->gpsdata.attitude.heading, session->gpsdata.attitude.mag_st); return mask; } static gps_mask_t processTNTA(int c UNUSED, char *field[], struct gps_device_t *session) { /* * Proprietary sentence for iSync GRClok/LNRClok. $PTNTA,20000102173852,1,T4,,,6,1,0*32 1. Date/time in format year, month, day, hour, minute, second 2. Oscillator quality 0:warming up, 1:freerun, 2:disciplined. 3. Always T4. Format indicator. 4. Interval ppsref-ppsout in [ns]. Blank if no ppsref. 5. Fine phase comparator in approx. [ns]. Always close to -500 or +500 if not disciplined. Blank if no ppsref. 6. iSync Status. 0:warming up or no light, 1:tracking set-up, 2:track to PPSREF, 3:synch to PPSREF, 4:Free Run. Track OFF, 5:FR. PPSREF unstable, 6:FR. No PPSREF, 7:FREEZE, 8:factory used, 9:searching Rb line 7. GPS messages indicator. 0:do not take account, 1:take account, but no message, 2:take account, partially ok, 3:take account, totally ok. 8. Transfer quality of date/time. 0:no, 1:manual, 2:GPS, older than x hours, 3:GPS, fresh. */ gps_mask_t mask = ONLINE_SET; if (strcmp(field[3], "T4") == 0) { struct oscillator_t *osc = &session->gpsdata.osc; unsigned int quality = atoi(field[2]); unsigned int delta = atoi(field[4]); unsigned int fine = atoi(field[5]); unsigned int status = atoi(field[6]); char deltachar = field[4][0]; osc->running = (quality > 0); osc->reference = (deltachar && (deltachar != '?')); if (osc->reference) { if (delta < 500) { osc->delta = fine; } else { osc->delta = ((delta < 500000000) ? delta : 1000000000 - delta); } } else { osc->delta = 0; } osc->disciplined = ((quality == 2) && (status == 3)); mask |= OSCILLATOR_SET; gpsd_log(&session->context->errout, LOG_DATA, "PTNTA,T4: quality=%s, delta=%s, fine=%s, status=%s\n", field[2], field[4], field[5], field[6]); } 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 Fields in order: 1. Azimuth 2. Pitch Angle 3. Roll Angle 4. Sensor temp, degrees centigrade 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 = ONLINE_SET; session->gpsdata.attitude.heading = safe_atof(field[1]); session->gpsdata.attitude.pitch = safe_atof(field[2]); session->gpsdata.attitude.roll = safe_atof(field[3]); session->gpsdata.attitude.temp = safe_atof(field[4]); session->gpsdata.attitude.depth = safe_atof(field[5]) / METERS_TO_FEET; session->gpsdata.attitude.mag_len = safe_atof(field[6]); session->gpsdata.attitude.mag_x = safe_atof(field[7]); session->gpsdata.attitude.mag_y = safe_atof(field[8]); session->gpsdata.attitude.mag_z = safe_atof(field[9]); session->gpsdata.attitude.acc_len = safe_atof(field[10]); session->gpsdata.attitude.acc_x = safe_atof(field[11]); session->gpsdata.attitude.acc_y = safe_atof(field[12]); session->gpsdata.attitude.acc_z = safe_atof(field[13]); session->gpsdata.attitude.gyro_x = safe_atof(field[15]); session->gpsdata.attitude.gyro_y = safe_atof(field[16]); mask |= (ATTITUDE_SET); gpsd_log(&session->context->errout, LOG_RAW, "Heading %lf.\n", session->gpsdata.attitude.heading); return mask; } #endif /* OCEANSERVER_ENABLE */ #ifdef ASHTECH_ENABLE /* Ashtech sentences take this format: * $PASHDR,type[,val[,val]]*CS * type is an alphabetic subsentence type * * Oxford Technical Solutions (OxTS) also uses the $PASHR sentence, * but with a very different sentence contents: * $PASHR,HHMMSS.SSS,HHH.HH,T,RRR.RR,PPP.PP,aaa.aa,r.rrr,p.ppp,h.hhh,Q1,Q2*CS * * so field 1 in ASHTECH is always alphabetic and numeric in OXTS * */ static gps_mask_t processPASHR(int c UNUSED, char *field[], struct gps_device_t *session) { gps_mask_t mask = ONLINE_SET; if (0 == strcmp("ACK", field[1])) { /* ACK */ gpsd_log(&session->context->errout, LOG_DATA, "PASHR,ACK\n"); return ONLINE_SET; } else if (0 == strcmp("MCA", field[1])) { /* MCA, raw data */ gpsd_log(&session->context->errout, LOG_DATA, "PASHR,MCA\n"); return ONLINE_SET; } else if (0 == strcmp("NAK", field[1])) { /* NAK */ gpsd_log(&session->context->errout, LOG_DATA, "PASHR,NAK\n"); return ONLINE_SET; } else if (0 == strcmp("PBN", field[1])) { /* PBN, position data */ /* FIXME: decode this for ECEF */ gpsd_log(&session->context->errout, LOG_DATA, "PASHR,PBN\n"); return ONLINE_SET; } else if (0 == strcmp("POS", field[1])) { /* 3D Position */ /* $PASHR,POS, * * 2: position type: * 0 = autonomous * 1 = position differentially corrected with RTCM code * 2 = position differentially corrected with CPD float solution * 3 = position is CPD fixed solution */ mask |= MODE_SET | STATUS_SET | CLEAR_IS; if (0 == strlen(field[2])) { /* empty first field means no 3D fix is available */ session->gpsdata.status = STATUS_NO_FIX; session->newdata.mode = MODE_NO_FIX; } else { int satellites_used; /* if we make it this far, we at least have a 3D fix */ session->newdata.mode = MODE_3D; if (1 <= atoi(field[2])) session->gpsdata.status = STATUS_DGPS_FIX; else session->gpsdata.status = STATUS_FIX; /* don't use as this breaks the GPGSV counter * session->gpsdata.satellites_used = atoi(field[3]); */ satellites_used = atoi(field[3]); if (0 == merge_hhmmss(field[4], session)) { register_fractional_time(field[0], field[4], session); mask |= TIME_SET; } if (0 == do_lat_lon(&field[5], &session->newdata)) { mask |= LATLON_SET; if ('\0' != field[9][0]) { session->newdata.altitude = safe_atof(field[9]); mask |= ALTITUDE_SET; } } session->newdata.track = safe_atof(field[11]); session->newdata.speed = safe_atof(field[12]) / MPS_TO_KPH; session->newdata.climb = safe_atof(field[13]); session->gpsdata.dop.pdop = safe_atof(field[14]); session->gpsdata.dop.hdop = safe_atof(field[15]); session->gpsdata.dop.vdop = safe_atof(field[16]); session->gpsdata.dop.tdop = safe_atof(field[17]); mask |= (SPEED_SET | TRACK_SET | CLIMB_SET); mask |= DOP_SET; gpsd_log(&session->context->errout, LOG_DATA, "PASHR,POS: hhmmss=%s lat=%.2f lon=%.2f alt=%.f" " speed=%.2f track=%.2f climb=%.2f mode=%d status=%d" " pdop=%.2f hdop=%.2f vdop=%.2f tdop=%.2f used=%d\n", field[4], session->newdata.latitude, session->newdata.longitude, session->newdata.altitude, session->newdata.speed, session->newdata.track, session->newdata.climb, session->newdata.mode, session->gpsdata.status, session->gpsdata.dop.pdop, session->gpsdata.dop.hdop, session->gpsdata.dop.vdop, session->gpsdata.dop.tdop, satellites_used); } } else if (0 == strcmp("RID", field[1])) { /* Receiver ID */ (void)snprintf(session->subtype, sizeof(session->subtype) - 1, "%s ver %s", field[2], field[3]); gpsd_log(&session->context->errout, LOG_DATA, "PASHR,RID: subtype=%s mask={}\n", session->subtype); return mask; } else if (0 == strcmp("SAT", field[1])) { /* Satellite Status */ struct satellite_t *sp; int i, n = session->gpsdata.satellites_visible = atoi(field[2]); session->gpsdata.satellites_used = 0; for (i = 0, sp = session->gpsdata.skyview; sp < session->gpsdata.skyview + n; sp++, i++) { sp->PRN = (short)atoi(field[3 + i * 5 + 0]); sp->azimuth = (short)atoi(field[3 + i * 5 + 1]); sp->elevation = (short)atoi(field[3 + i * 5 + 2]); sp->ss = safe_atof(field[3 + i * 5 + 3]); sp->used = false; if (field[3 + i * 5 + 4][0] == 'U') { sp->used = true; session->gpsdata.satellites_used++; } } gpsd_log(&session->context->errout, LOG_DATA, "PASHR,SAT: used=%d\n", session->gpsdata.satellites_used); session->gpsdata.skyview_time = NAN; mask |= SATELLITE_SET | USED_IS; } else if (0 == strcmp("T", field[3])) { /* Assume OxTS PASHR */ /* FIXME: decode OxTS $PASHDR, time is wrong, breaks cycle order */ if (0 == merge_hhmmss(field[1], session)) { register_fractional_time(field[0], field[1], session); /* mask |= TIME_SET; confuses cycle order */ } session->gpsdata.attitude.heading = safe_atof(field[2]); session->gpsdata.attitude.roll = safe_atof(field[4]); session->gpsdata.attitude.pitch = safe_atof(field[5]); /* mask |= ATTITUDE_SET; * confuses cycle order ?? */ gpsd_log(&session->context->errout, LOG_RAW, "PASHR (OxTS) time %.3f, heading %lf.\n", session->newdata.time, session->gpsdata.attitude.heading); } return mask; } #endif /* ASHTECH_ENABLE */ #ifdef MTK3301_ENABLE static gps_mask_t processMTK3301(int c UNUSED, char *field[], struct gps_device_t *session) { int msg, reason; msg = atoi(&(session->nmea.field[0])[4]); switch (msg) { case 001: /* ACK / NACK */ reason = atoi(field[2]); if (atoi(field[1]) == -1) gpsd_log(&session->context->errout, LOG_WARN, "MTK NACK: unknown sentence\n"); else if (reason < 3) { const char *mtk_reasons[] = { "Invalid", "Unsupported", "Valid but Failed", "Valid success" }; gpsd_log(&session->context->errout, LOG_WARN, "MTK NACK: %s, reason: %s\n", field[1], mtk_reasons[reason]); } else gpsd_log(&session->context->errout, LOG_DATA, "MTK ACK: %s\n", field[1]); return ONLINE_SET; case 424: /* PPS pulse width response */ /* * Response will look something like: $PMTK424,0,0,1,0,69*12 * The pulse width is in field 5 (69 in this example). This * sentence is poorly documented at: * http://www.trimble.com/embeddedsystems/condor-gps-module.aspx?dtID=documentation * * Packet Type: 324 PMTK_API_SET_OUTPUT_CTL * Packet meaning * Write the TSIP / antenna / PPS configuration data to the Flash memory. * DataField [Data0]:TSIP Packet[on/off] * 0 - Disable TSIP output (Default). * 1 - Enable TSIP output. * [Data1]:Antenna Detect[on/off] * 0 - Disable antenna detect function (Default). * 1 - Enable antenna detect function. * [Data2]:PPS on/off * 0 - Disable PPS function. * 1 - Enable PPS function (Default). * [Data3]:PPS output timing * 0 - Always output PPS (Default). * 1 - Only output PPS when GPS position is fixed. * [Data4]:PPS pulse width * 1~16367999: 61 ns~(61x 16367999) ns (Default = 69) * * The documentation does not give the units of the data field. * Andy Walls says: * * "The best I can figure using an oscilloscope, is that it is * in units of 16.368000 MHz clock cycles. It may be * different for any other unit other than the Trimble * Condor. 69 cycles / 16368000 cycles/sec = 4.216 microseconds * [which is the pulse width I have observed]" * * Support for this theory comes from the fact that crystal * TXCOs with a 16.368MHZ period are commonly available from * multiple vendors. Furthermore, 61*69 = 4209, which is * close to the observed cycle time and suggests that the * documentation is trying to indicate 61ns units. * * He continues: * * "I chose [127875] because to divides 16368000 nicely and the * pulse width is close to 1/100th of a second. Any number * the user wants to use would be fine. 127875 cycles / * 16368000 cycles/second = 1/128 seconds = 7.8125 * milliseconds" */ /* too short? Make it longer */ if (atoi(field[5]) < 127875) (void)nmea_send(session, "$PMTK324,0,0,1,0,127875"); return ONLINE_SET; case 705: /* return device subtype */ (void)strlcat(session->subtype, field[1], sizeof(session->subtype)); (void)strlcat(session->subtype, "-", sizeof(session->subtype)); (void)strlcat(session->subtype, field[2], sizeof(session->subtype)); return ONLINE_SET; default: gpsd_log(&session->context->errout, LOG_PROG, "MTK: unknown msg: %d\n", msg); return ONLINE_SET; /* ignore */ } } #endif /* MTK3301_ENABLE */ #ifdef SKYTRAQ_ENABLE /* Recommended Minimum 3D GNSS Data */ static gps_mask_t processPSTI030(int count, char *field[], struct gps_device_t *session) { /* * $PSTI,030,hhmmss.sss,A,dddmm.mmmmmmm,a,dddmm.mmmmmmm,a,x.x,x.x,x.x,x.x,ddmmyy,a.x.x,x.x*hh * 1 030 Sentence ID * 2 225446.334 Time of fix 22:54:46 UTC * 3 A Status of Fix: A = Autonomous, valid; * V = invalid * 4,5 4916.45,N Latitude 49 deg. 16.45 min North * 6,7 12311.12,W Longitude 123 deg. 11.12 min West * 8 103.323 Mean Sea Level meters * 9 0.00 East Velocity meters/sec * 10 0.00 North Velocity meters/sec * 11 0.00 Up Velocity meters/sec * 12 181194 Date of fix 18 November 1994 * 13 A FAA mode indicator * See faa_mode() for possible mode values. * 14 1.2 RTK Age * 15 4.2 RTK Ratio * 16 *68 mandatory nmea_checksum * * In private email, SkyTraq says F mode is 10x more accurate * than R mode. */ gps_mask_t mask = ONLINE_SET; if ( 16 != count ) return mask; if ('V' == field[3][0] || 'N' == field[13][0]) { /* nav warning, or FAA not valid, ignore the rest of the data */ session->gpsdata.status = STATUS_NO_FIX; session->newdata.mode = MODE_NO_FIX; mask |= MODE_SET; } else if ('A' == field[3][0]) { double east, north; /* data valid */ if (field[2][0] != '\0' && field[12][0] != '\0') { /* good date and time */ if (0 == merge_hhmmss(field[2], session) && 0 == merge_ddmmyy(field[12], session)) { mask |= TIME_SET; register_fractional_time( "PSTI030", field[2], session); } } if (0 == do_lat_lon(&field[4], &session->newdata)) { session->newdata.mode = MODE_2D; mask |= LATLON_SET; if ('\0' != field[8][0]) { session->newdata.altitude = safe_atof(field[8]); mask |= ALTITUDE_SET; session->newdata.mode = MODE_3D; } mask |= MODE_SET; } /* convert ENU to speed/track */ /* this has more precision than GPVTG, GPVTG comes earlier * in the cycle */ east = safe_atof(field[9]); /* east velocity m/s */ north = safe_atof(field[10]); /* north velocity m/s */ session->newdata.speed = sqrt(pow(east, 2) + pow(north, 2)); session->newdata.track = atan2(east, north) * RAD_2_DEG; if ( 0 > session->newdata.track ) session->newdata.track += 360.0; /* up velocity m/s */ session->newdata.climb = safe_atof(field[11]); mask |= SPEED_SET | TRACK_SET | CLIMB_SET; session->gpsdata.status = faa_mode(field[13][0]); /* Ignore RTK Age and RTK Ratio, for now */ } gpsd_log(&session->context->errout, LOG_DATA, "PSTI,030: ddmmyy=%s hhmmss=%s lat=%.2f lon=%.2f " "status=%d, RTK(Age=%s Ratio=%s)\n", field[12], field[2], session->newdata.latitude, session->newdata.longitude, session->gpsdata.status, field[14], field[15]); return mask; } /* * Skytraq sentences take this format: * $PSTI,type[,val[,val]]*CS * type is a 2 or 3 digit subsentence type * * Note: this sentence can be at least 100 chars long. * That violates the NMEA 3.01 max of 82. * */ static gps_mask_t processPSTI(int count, char *field[], struct gps_device_t *session) { gps_mask_t mask = ONLINE_SET; if ( 0 != strncmp(session->subtype, "Skytraq", 7) ) { /* this is skytraq, but not marked yet, so probe for Skytraq */ (void)gpsd_write(session, "\xA0\xA1\x00\x02\x02\x01\x03\x0d\x0a",9); } if (0 == strcmp("00", field[1]) ) { if ( 4 != count ) return mask; /* 1 PPS Timing report ID */ gpsd_log(&session->context->errout, LOG_DATA, "PSTI,00: Mode: %s, Length: %s, Quant: %s\n", field[2], field[3], field[4]); return mask; } if (0 == strcmp("001", field[1])) { /* Active Antenna Status Report */ gpsd_log(&session->context->errout, LOG_DATA, "PSTI,001: Count: %d\n", count); return mask; } if (0 == strcmp("005", field[1])) { /* GPIO 10 event-triggered time & position stamp. */ gpsd_log(&session->context->errout, LOG_DATA, "PSTI,005: Count: %d\n", count); return mask; } if (0 == strcmp("030", field[1])) { /* Recommended Minimum 3D GNSS Data */ return processPSTI030(count, field, session); } if (0 == strcmp("032", field[1])) { if ( 16 != count ) return mask; /* RTK Baseline */ if ( 0 == strcmp(field[4], "A")) { /* Status Valid */ if (field[2][0] != '\0' && field[3][0] != '\0') { /* good date and time */ if (0 == merge_hhmmss(field[2], session) && 0 == merge_ddmmyy(field[3], session)) { mask |= TIME_SET; register_fractional_time( "PSTI032", field[2], session); } } } gpsd_log(&session->context->errout, LOG_DATA, "PSTI,032: stat:%s mode: %s E: %s N: %s U:%s L:%s C:%s\n", field[4], field[5], field[6], field[7], field[8], field[9], field[10]); return mask; } gpsd_log(&session->context->errout, LOG_DATA, "PSTI,%s: Unknown type, Count: %d\n", field[1], count); return mask; } /* * Skytraq undocumented debug sentences take this format: * $STI,type,val*CS * type is a 2 char subsentence type * Note: NO checksum */ static gps_mask_t processSTI(int count, char *field[], struct gps_device_t *session) { gps_mask_t mask = ONLINE_SET; if ( 0 != strncmp(session->subtype, "Skytraq", 7) ) { /* this is skytraq, but marked yet, so probe for Skytraq */ (void)gpsd_write(session, "\xA0\xA1\x00\x02\x02\x01\x03\x0d\x0a",9); } if ( 0 == strcmp( field[1], "IC") ) { /* $STI,IC,error=XX, this is always very bad, but undocumented */ gpsd_log(&session->context->errout, LOG_ERROR, "Skytraq: $STI,%s,%s\n", field[1], field[2]); return mask; } gpsd_log(&session->context->errout, LOG_DATA, "STI,%s: Unknown type, Count: %d\n", field[1], count); return mask; } #endif /* SKYTRAQ_ENABLE */ /************************************************************************** * * Entry points begin here * **************************************************************************/ 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 */ bool cycle_continue; /* cycle continuer? */ nmea_decoder decoder; } nmea_phrase[] = { {"PGRMB", 0, false, NULL}, /* ignore Garmin DGPS Beacon Info */ {"PGRMC", 0, false, NULL}, /* ignore Garmin Sensor Config */ {"PGRME", 7, false, processPGRME}, {"PGRMF", 15, false, processPGRMF}, /* Garmin GPS Fix Data */ {"PGRMH", 0, false, NULL}, /* ignore Garmin Aviation Height... */ {"PGRMI", 0, false, NULL}, /* ignore Garmin Sensor Init */ {"PGRMM", 2, false, processPGRMM}, /* Garmin Map Datum */ {"PGRMO", 0, false, NULL}, /* ignore Garmin Sentence Enable */ {"PGRMT", 0, false, NULL}, /* ignore Garmin Sensor Info */ {"PGRMV", 0, false, NULL}, /* ignore Garmin 3D Velocity Info */ {"PGRMZ", 4, false, processPGRMZ}, /* * Basic sentences must come after the PG* ones, otherwise * Garmins can get stuck in a loop that looks like this: * * 1. A Garmin GPS in NMEA mode is detected. * * 2. PGRMC is sent to reconfigure to Garmin binary mode. * If successful, the GPS echoes the phrase. * * 3. nmea_parse() sees the echo as RMC because the talker * ID is ignored, and fails to recognize the echo as * PGRMC and ignore it. * * 4. The mode is changed back to NMEA, resulting in an * infinite loop. */ {"AAM", 0, false, NULL}, /* ignore Waypoint Arrival Alarm */ {"APB", 0, false, NULL}, /* ignore Autopilot Sentence B */ {"BOD", 0, false, NULL}, /* ignore Bearing Origin to Destination */ /* ignore Bearing & Distance to Waypoint, Great Circle*/ {"BWC", 0, false, NULL}, {"DBT", 7, true, processDBT}, {"DPT", 0, false, NULL}, /* ignore depth */ {"DTM", 2, false, processDTM}, /* datum */ {"GBS", 7, false, processGBS}, {"GGA", 13, false, processGGA}, {"GLC", 0, false, NULL}, /* ignore Geographic Position, LoranC */ {"GLL", 7, false, processGLL}, {"GNS", 13, false, processGNS}, {"GRS", 0, false, NULL}, /* ignore GNSS Range Residuals */ {"GSA", 18, false, processGSA}, {"GST", 8, false, processGST}, {"GSV", 0, false, processGSV}, /* ignore Heading, Deviation and Variation */ {"HDG", 0, false, NULL}, {"HDT", 1, false, processHDT}, {"MSS", 0, false, NULL}, /* ignore beacon receiver status */ {"MTW", 0, false, NULL}, /* ignore Water Temperature */ {"MWD", 0, false, NULL}, /* ignore Wind Direction and Speed */ {"MWV", 0, false, NULL}, /* ignore Wind Speed and Angle */ #ifdef OCEANSERVER_ENABLE {"OHPR", 18, false, processOHPR}, #endif /* OCEANSERVER_ENABLE */ #ifdef ASHTECH_ENABLE /* general handler for Ashtech */ {"PASHR", 3, false, processPASHR}, #endif /* ASHTECH_ENABLE */ {"PMGNST", 8, false, processPMGNST}, /* Magellan Status */ #ifdef MTK3301_ENABLE {"PMTK", 3, false, processMTK3301}, /* for some reason the parser no longer triggering on leading chars */ {"PMTK001", 3, false, processMTK3301}, {"PMTK424", 3, false, processMTK3301}, {"PMTK705", 3, false, processMTK3301}, {"PMTKCHN", 0, false, NULL}, /* ignore MediaTek Channel Status */ #endif /* MTK3301_ENABLE */ {"PRWIZCH", 0, false, NULL}, /* ignore Rockwell Channel Status */ {"PSRFEPE", 7, false, processPSRFEPE}, /* SiRF Estimated Errors */ {"PTFTTXT", 0, false, NULL}, /* ignore unknown uptime */ {"PUBX", 0, false, NULL}, /* ignore u-blox Antaris */ #ifdef TNT_ENABLE {"PTNTHTM", 9, false, processTNTHTM}, {"PTNTA", 8, false, processTNTA}, #endif /* TNT_ENABLE */ #ifdef SKYTRAQ_ENABLE {"PSTI", 2, false, processPSTI}, /* $PSTI Skytraq */ {"STI", 2, false, processSTI}, /* $STI Skytraq */ #endif /* SKYTRAQ_ENABLE */ /* ignore Recommended Minimum Navigation Info, waypoint */ {"RMB", 0, false, NULL}, {"RMC", 8, false, processRMC}, {"RTE", 0, false, NULL}, /* ignore Routes */ {"TXT", 5, false, processTXT}, {"VHW", 0, false, NULL}, /* ignore Water Speed and Heading */ {"VLW", 0, false, NULL}, /* ignore Dual ground/water distance */ {"VTG", 5, false, processVTG}, {"XDR", 0, false, NULL}, /* ignore $HCXDR, IMU? */ {"XTE", 0, false, NULL}, /* ignore Cross-Track Error */ {"ZDA", 4, false, processZDA}, }; int count; gps_mask_t mask = 0; unsigned int i, thistag, lasttag; char *p, *e; volatile char *t; uint64_t lasttag_mask = 0; uint64_t thistag_mask = 0; #ifdef SKYTRAQ_ENABLE bool skytraq_sti = false; #endif /* * 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_log(&session->context->errout, LOG_WARN, "Overlong packet of %zd chars rejected.\n", strlen(sentence)); return ONLINE_SET; } /* make an editable copy of the sentence */ (void)strlcpy((char *)session->nmea.fieldcopy, sentence, sizeof(session->nmea.fieldcopy) - 1); /* discard the checksum part */ for (p = (char *)session->nmea.fieldcopy; (*p != '*') && (*p >= ' ');) ++p; if (*p == '*') *p++ = ','; /* otherwise we drop the last field */ #ifdef SKYTRAQ_ENABLE_UNUSED /* $STI is special, no trailing *, or chacksum */ if ( 0 != strncmp( "STI,", sentence, 4) ) { skytraq_sti = true; *p++ = ','; /* otherwise we drop the last field */ } #endif *p = '\0'; e = p; /* split sentence copy on commas, filling the field array */ count = 0; t = p; /* end of sentence */ p = (char *)session->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->nmea.field[count] = p; /* we have a field. record it */ if ((p = strchr(p, ',')) != NULL) { /* search for the next delimiter */ *p = '\0'; /* replace it with a NUL */ count++; /* bump the counters and continue */ p++; } } /* point remaining fields at empty string, just in case */ for (i = (unsigned int)count; i < (unsigned)(sizeof(session->nmea.field) / sizeof(session->nmea.field[0])); i++) session->nmea.field[i] = e; /* sentences handlers will tell us when they have fractional time */ session->nmea.latch_frac_time = false; /* dispatch on field zero, the sentence tag */ for (thistag = i = 0; i < (unsigned)(sizeof(nmea_phrase) / sizeof(nmea_phrase[0])); ++i) { char *s = session->nmea.field[0]; if (strlen(nmea_phrase[i].name) == 3 #ifdef SKYTRAQ_ENABLE /* $STI is special */ && !skytraq_sti #endif ) { s += 2; /* skip talker ID */ } if (strcmp(nmea_phrase[i].name, s) == 0) { if (NULL == nmea_phrase[i].decoder) { /* no decoder for this sentence */ mask = ONLINE_SET; gpsd_log(&session->context->errout, LOG_DATA, "No decoder for sentence %s\n", session->nmea.field[0]); break; } if (count < nmea_phrase[i].nf) { /* sentence to short */ mask = ONLINE_SET; gpsd_log(&session->context->errout, LOG_DATA, "Sentence %s too short\n", session->nmea.field[0]); break; } mask = (nmea_phrase[i].decoder)(count, session->nmea.field, session); if (nmea_phrase[i].cycle_continue) session->nmea.cycle_continue = true; /* * Must force this to be nz, as we're going to rely on a zero * value to mean "no previous tag" later. */ thistag = i + 1; break; } } /* prevent overaccumulation of sat reports */ if (!str_starts_with(session->nmea.field[0] + 2, "GSV")) session->nmea.last_gsv_talker = '\0'; if (!str_starts_with(session->nmea.field[0] + 2, "GSA")) session->nmea.last_gsa_talker = '\0'; /* timestamp recording for fixes happens here */ if ((mask & TIME_SET) != 0) { session->newdata.time = gpsd_utc_resolve(session); gpsd_log(&session->context->errout, LOG_DATA, "%s time is %.3f = %d-%02d-%02dT%02d:%02d:%.3fZ\n", session->nmea.field[0], session->newdata.time, 1900 + session->nmea.date.tm_year, session->nmea.date.tm_mon + 1, session->nmea.date.tm_mday, session->nmea.date.tm_hour, session->nmea.date.tm_min, session->nmea.date.tm_sec + session->nmea.subseconds); /* * If we have time and PPS is available, assume we have good time. * Because this is a generic driver we don't really have enough * information for a sharper test, so we'll leave it up to the * PPS code to do its own sanity filtering. */ mask |= NTPTIME_IS; } /* * The end-of-cycle detector. This code depends on just one * assumption: if a sentence with a timestamp occurs just before * start of cycle, then it is always good to trigger a report on * that sentence in the future. For devices with a fixed cycle * this should work perfectly, locking in detection after one * cycle. Most split-cycle devices (Garmin 48, for example) will * work fine. Problems will only arise if a a sentence that * occurs just before timestamp increments also occurs in * mid-cycle, as in the Garmin eXplorist 210; those might jitter. */ /* cast for 32/64 bit compat */ gpsd_log(&session->context->errout, LOG_DATA, "%s time %.3f last %.3f latch %d cont %d enders %#llx\n", session->nmea.field[0], session->nmea.this_frac_time, session->nmea.last_frac_time, session->nmea.latch_frac_time, session->nmea.cycle_continue, (unsigned long long)session->nmea.cycle_enders); lasttag = session->nmea.lasttag; if (0 < session->nmea.lasttag) { lasttag_mask = (uint64_t)1 << lasttag; } if (0 < thistag) { thistag_mask = (uint64_t)1 << thistag; } if (session->nmea.latch_frac_time) { if (!GPS_TIME_EQUAL(session->nmea.this_frac_time, session->nmea.last_frac_time)) { mask |= CLEAR_IS; gpsd_log(&session->context->errout, LOG_PROG, "%s starts a reporting cycle. lasttag %d\n", session->nmea.field[0], lasttag); /* * Have we seen a previously timestamped NMEA tag? * If so, designate as end-of-cycle marker. * But not if there are continuation sentences; * those get sorted after the last timestamped sentence * */ if (0 < lasttag && 0 == (session->nmea.cycle_enders & lasttag_mask) && !session->nmea.cycle_continue) { session->nmea.cycle_enders |= lasttag_mask; /* cast for 32/64 bit compat */ gpsd_log(&session->context->errout, LOG_PROG, "tagged %s as a cycle ender. %#llx\n", nmea_phrase[lasttag - 1].name, (unsigned long long)lasttag_mask); } } } else { /* extend the cycle to an un-timestamped sentence? */ if (0 != (session->nmea.cycle_enders & lasttag_mask)) { gpsd_log(&session->context->errout, LOG_PROG, "%s is just after a cycle ender.\n", session->nmea.field[0]); } if (session->nmea.cycle_continue) { gpsd_log(&session->context->errout, LOG_PROG, "%s extends the reporting cycle.\n", session->nmea.field[0]); /* change ender */ session->nmea.cycle_enders &= ~lasttag_mask; session->nmea.cycle_enders |= thistag_mask; } } /* here's where we check for end-of-cycle */ if ((session->nmea.latch_frac_time || session->nmea.cycle_continue) && (session->nmea.cycle_enders & thistag_mask)!=0) { gpsd_log(&session->context->errout, LOG_PROG, "%s ends a reporting cycle.\n", session->nmea.field[0]); mask |= REPORT_IS; } if (session->nmea.latch_frac_time) session->nmea.lasttag = thistag; /* we might have a (somewhat) reliable end-of-cycle */ if (session->nmea.cycle_enders != 0) session->cycle_end_reliable = true; /* don't downgrade mode if holding previous fix */ /* usually because of xxRMC which does not report 2D/3D */ if (MODE_SET == (mask & MODE_SET) && MODE_3D == session->gpsdata.fix.mode && MODE_NO_FIX != session->newdata.mode && (0 != isfinite(session->lastfix.altitude) || 0 != isfinite(session->oldfix.altitude))) { session->newdata.mode = session->gpsdata.fix.mode; } return mask; } #endif /* NMEA0183_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 == '!') { p++; } 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)); }