/* $OpenBSD: tty_nmea.c,v 1.51 2022/04/02 22:45:18 mlarkin Exp $ */ /* * Copyright (c) 2006, 2007, 2008 Marc Balmer * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* * A tty line discipline to decode NMEA 0183 data to get the time * and GPS position data */ #include #include #include #include #include #include #include #ifdef NMEA_DEBUG #define DPRINTFN(n, x) do { if (nmeadebug > (n)) printf x; } while (0) int nmeadebug = 0; #else #define DPRINTFN(n, x) #endif #define DPRINTF(x) DPRINTFN(0, x) void nmeaattach(int); #define NMEAMAX 82 #define MAXFLDS 32 #define KNOTTOMS (51444 / 100) #ifdef NMEA_DEBUG #define TRUSTTIME 30 #else #define TRUSTTIME (10 * 60) /* 10 minutes */ #endif int nmea_count, nmea_nxid; struct nmea { char cbuf[NMEAMAX]; /* receive buffer */ struct ksensor time; /* the timedelta sensor */ struct ksensor signal; /* signal status */ struct ksensor latitude; struct ksensor longitude; struct ksensor altitude; struct ksensor speed; struct ksensordev timedev; struct timespec ts; /* current timestamp */ struct timespec lts; /* timestamp of last '$' seen */ struct timeout nmea_tout; /* invalidate sensor */ int64_t gap; /* gap between two sentences */ #ifdef NMEA_DEBUG int gapno; #endif int64_t last; /* last time rcvd */ int sync; /* if 1, waiting for '$' */ int pos; /* position in rcv buffer */ int no_pps; /* no PPS although requested */ char mode; /* GPS mode */ }; /* NMEA decoding */ void nmea_scan(struct nmea *, struct tty *); void nmea_gprmc(struct nmea *, struct tty *, char *fld[], int fldcnt); void nmea_decode_gga(struct nmea *, struct tty *, char *fld[], int fldcnt); /* date and time conversion */ int nmea_date_to_nano(char *s, int64_t *nano); int nmea_time_to_nano(char *s, int64_t *nano); /* longitude and latitude conversion */ int nmea_degrees(int64_t *dst, char *src, int neg); int nmea_atoi(int64_t *dst, char *src); /* degrade the timedelta sensor */ void nmea_timeout(void *); void nmeaattach(int dummy) { /* noop */ } int nmeaopen(dev_t dev, struct tty *tp, struct proc *p) { struct nmea *np; int error; if (tp->t_line == NMEADISC) return (ENODEV); if ((error = suser(p)) != 0) return (error); np = malloc(sizeof(struct nmea), M_DEVBUF, M_WAITOK | M_ZERO); snprintf(np->timedev.xname, sizeof(np->timedev.xname), "nmea%d", nmea_nxid++); nmea_count++; np->time.status = SENSOR_S_UNKNOWN; np->time.type = SENSOR_TIMEDELTA; np->time.flags = SENSOR_FINVALID; sensor_attach(&np->timedev, &np->time); np->signal.type = SENSOR_INDICATOR; np->signal.status = SENSOR_S_UNKNOWN; np->signal.value = 0; strlcpy(np->signal.desc, "Signal", sizeof(np->signal.desc)); sensor_attach(&np->timedev, &np->signal); np->latitude.type = SENSOR_ANGLE; np->latitude.status = SENSOR_S_UNKNOWN; np->latitude.flags = SENSOR_FINVALID; np->latitude.value = 0; strlcpy(np->latitude.desc, "Latitude", sizeof(np->latitude.desc)); sensor_attach(&np->timedev, &np->latitude); np->longitude.type = SENSOR_ANGLE; np->longitude.status = SENSOR_S_UNKNOWN; np->longitude.flags = SENSOR_FINVALID; np->longitude.value = 0; strlcpy(np->longitude.desc, "Longitude", sizeof(np->longitude.desc)); sensor_attach(&np->timedev, &np->longitude); np->altitude.type = SENSOR_DISTANCE; np->altitude.status = SENSOR_S_UNKNOWN; np->altitude.flags = SENSOR_FINVALID; np->altitude.value = 0; strlcpy(np->altitude.desc, "Altitude", sizeof(np->altitude.desc)); sensor_attach(&np->timedev, &np->altitude); np->speed.type = SENSOR_VELOCITY; np->speed.status = SENSOR_S_UNKNOWN; np->speed.flags = SENSOR_FINVALID; np->speed.value = 0; strlcpy(np->speed.desc, "Ground speed", sizeof(np->speed.desc)); sensor_attach(&np->timedev, &np->speed); np->sync = 1; tp->t_sc = (caddr_t)np; error = linesw[TTYDISC].l_open(dev, tp, p); if (error) { free(np, M_DEVBUF, sizeof(*np)); tp->t_sc = NULL; } else { sensordev_install(&np->timedev); timeout_set(&np->nmea_tout, nmea_timeout, np); } return (error); } int nmeaclose(struct tty *tp, int flags, struct proc *p) { struct nmea *np = (struct nmea *)tp->t_sc; tp->t_line = TTYDISC; /* switch back to termios */ timeout_del(&np->nmea_tout); sensordev_deinstall(&np->timedev); free(np, M_DEVBUF, sizeof(*np)); tp->t_sc = NULL; nmea_count--; if (nmea_count == 0) nmea_nxid = 0; return (linesw[TTYDISC].l_close(tp, flags, p)); } /* Collect NMEA sentences from the tty. */ int nmeainput(int c, struct tty *tp) { struct nmea *np = (struct nmea *)tp->t_sc; struct timespec ts; int64_t gap; long tmin, tmax; switch (c) { case '$': nanotime(&ts); np->pos = np->sync = 0; gap = (ts.tv_sec * 1000000000LL + ts.tv_nsec) - (np->lts.tv_sec * 1000000000LL + np->lts.tv_nsec); np->lts.tv_sec = ts.tv_sec; np->lts.tv_nsec = ts.tv_nsec; if (gap <= np->gap) break; np->ts.tv_sec = ts.tv_sec; np->ts.tv_nsec = ts.tv_nsec; #ifdef NMEA_DEBUG if (nmeadebug > 0) { linesw[TTYDISC].l_rint('[', tp); linesw[TTYDISC].l_rint('0' + np->gapno++, tp); linesw[TTYDISC].l_rint(']', tp); } #endif np->gap = gap; /* * If a tty timestamp is available, make sure its value is * reasonable by comparing against the timestamp just taken. * If they differ by more than 2 seconds, assume no PPS signal * is present, note the fact, and keep using the timestamp * value. When this happens, the sensor state is set to * CRITICAL later when the GPRMC sentence is decoded. */ if (tp->t_flags & (TS_TSTAMPDCDSET | TS_TSTAMPDCDCLR | TS_TSTAMPCTSSET | TS_TSTAMPCTSCLR)) { tmax = lmax(np->ts.tv_sec, tp->t_tv.tv_sec); tmin = lmin(np->ts.tv_sec, tp->t_tv.tv_sec); if (tmax - tmin > 1) np->no_pps = 1; else { np->ts.tv_sec = tp->t_tv.tv_sec; np->ts.tv_nsec = tp->t_tv.tv_usec * 1000L; np->no_pps = 0; } } break; case '\r': case '\n': if (!np->sync) { np->cbuf[np->pos] = '\0'; nmea_scan(np, tp); np->sync = 1; } break; default: if (!np->sync && np->pos < (NMEAMAX - 1)) np->cbuf[np->pos++] = c; break; } /* pass data to termios */ return (linesw[TTYDISC].l_rint(c, tp)); } /* Scan the NMEA sentence just received. */ void nmea_scan(struct nmea *np, struct tty *tp) { int fldcnt = 0, cksum = 0, msgcksum, n; char *fld[MAXFLDS], *cs; /* split into fields and calculate the checksum */ fld[fldcnt++] = &np->cbuf[0]; /* message type */ for (cs = NULL, n = 0; n < np->pos && cs == NULL; n++) { switch (np->cbuf[n]) { case '*': np->cbuf[n] = '\0'; cs = &np->cbuf[n + 1]; break; case ',': if (fldcnt < MAXFLDS) { cksum ^= np->cbuf[n]; np->cbuf[n] = '\0'; fld[fldcnt++] = &np->cbuf[n + 1]; } else { DPRINTF(("nr of fields in %s sentence exceeds " "maximum of %d\n", fld[0], MAXFLDS)); return; } break; default: cksum ^= np->cbuf[n]; } } /* * we only look at the messages coming from well-known sources or 'talkers', * distinguished by the two-chars prefix, the most common being: * GPS (GP) * Glonass (GL) * BeiDou (BD) * Galileo (GA) * 'Any kind/a mix of GNSS systems' (GN) */ if (strncmp(fld[0], "BD", 2) && strncmp(fld[0], "GA", 2) && strncmp(fld[0], "GL", 2) && strncmp(fld[0], "GN", 2) && strncmp(fld[0], "GP", 2)) return; /* we look for the RMC & GGA messages */ if (strncmp(fld[0] + 2, "RMC", 3) && strncmp(fld[0] + 2, "GGA", 3)) return; /* if we have a checksum, verify it */ if (cs != NULL) { msgcksum = 0; while (*cs) { if ((*cs >= '0' && *cs <= '9') || (*cs >= 'A' && *cs <= 'F')) { if (msgcksum) msgcksum <<= 4; if (*cs >= '0' && *cs<= '9') msgcksum += *cs - '0'; else if (*cs >= 'A' && *cs <= 'F') msgcksum += 10 + *cs - 'A'; cs++; } else { DPRINTF(("bad char %c in checksum\n", *cs)); return; } } if (msgcksum != cksum) { DPRINTF(("checksum mismatch\n")); return; } } if (strncmp(fld[0] + 2, "RMC", 3) == 0) nmea_gprmc(np, tp, fld, fldcnt); if (strncmp(fld[0] + 2, "GGA", 3) == 0) nmea_decode_gga(np, tp, fld, fldcnt); } /* Decode the recommended minimum specific GPS/TRANSIT data. */ void nmea_gprmc(struct nmea *np, struct tty *tp, char *fld[], int fldcnt) { int64_t date_nano, time_nano, nmea_now; int jumped = 0; if (fldcnt < 12 || fldcnt > 14) { DPRINTF(("gprmc: field count mismatch, %d\n", fldcnt)); return; } if (nmea_time_to_nano(fld[1], &time_nano)) { DPRINTF(("gprmc: illegal time, %s\n", fld[1])); return; } if (nmea_date_to_nano(fld[9], &date_nano)) { DPRINTF(("gprmc: illegal date, %s\n", fld[9])); return; } nmea_now = date_nano + time_nano; if (nmea_now <= np->last) { DPRINTF(("gprmc: time not monotonically increasing\n")); jumped = 1; } np->last = nmea_now; np->gap = 0LL; #ifdef NMEA_DEBUG if (np->time.status == SENSOR_S_UNKNOWN) { np->time.status = SENSOR_S_OK; timeout_add_sec(&np->nmea_tout, TRUSTTIME); } np->gapno = 0; if (nmeadebug > 0) { linesw[TTYDISC].l_rint('[', tp); linesw[TTYDISC].l_rint('C', tp); linesw[TTYDISC].l_rint(']', tp); } #endif np->time.value = np->ts.tv_sec * 1000000000LL + np->ts.tv_nsec - nmea_now; np->time.tv.tv_sec = np->ts.tv_sec; np->time.tv.tv_usec = np->ts.tv_nsec / 1000L; if (fldcnt < 13) strlcpy(np->time.desc, "GPS", sizeof(np->time.desc)); else if (*fld[12] != np->mode) { np->mode = *fld[12]; switch (np->mode) { case 'S': strlcpy(np->time.desc, "GPS simulated", sizeof(np->time.desc)); break; case 'E': strlcpy(np->time.desc, "GPS estimated", sizeof(np->time.desc)); break; case 'A': strlcpy(np->time.desc, "GPS autonomous", sizeof(np->time.desc)); break; case 'D': strlcpy(np->time.desc, "GPS differential", sizeof(np->time.desc)); break; case 'N': strlcpy(np->time.desc, "GPS invalid", sizeof(np->time.desc)); break; default: strlcpy(np->time.desc, "GPS unknown", sizeof(np->time.desc)); DPRINTF(("gprmc: unknown mode '%c'\n", np->mode)); } } switch (*fld[2]) { case 'A': /* The GPS has a fix, (re)arm the timeout. */ /* XXX is 'D' also a valid state? */ np->time.status = SENSOR_S_OK; np->signal.value = 1; np->signal.status = SENSOR_S_OK; np->latitude.status = SENSOR_S_OK; np->longitude.status = SENSOR_S_OK; np->speed.status = SENSOR_S_OK; np->time.flags &= ~SENSOR_FINVALID; np->latitude.flags &= ~SENSOR_FINVALID; np->longitude.flags &= ~SENSOR_FINVALID; np->speed.flags &= ~SENSOR_FINVALID; break; case 'V': /* * The GPS indicates a warning status, do not add to * the timeout, if the condition persist, the sensor * will be degraded. Signal the condition through * the signal sensor. */ np->signal.value = 0; np->signal.status = SENSOR_S_CRIT; np->latitude.status = SENSOR_S_WARN; np->longitude.status = SENSOR_S_WARN; np->speed.status = SENSOR_S_WARN; break; } if (nmea_degrees(&np->latitude.value, fld[3], *fld[4] == 'S' ? 1 : 0)) np->latitude.status = SENSOR_S_WARN; if (nmea_degrees(&np->longitude.value,fld[5], *fld[6] == 'W' ? 1 : 0)) np->longitude.status = SENSOR_S_WARN; if (nmea_atoi(&np->speed.value, fld[7])) np->speed.status = SENSOR_S_WARN; /* convert from knot to um/s */ np->speed.value *= KNOTTOMS; if (jumped) np->time.status = SENSOR_S_WARN; if (np->time.status == SENSOR_S_OK) timeout_add_sec(&np->nmea_tout, TRUSTTIME); /* * If tty timestamping is requested, but no PPS signal is present, set * the sensor state to CRITICAL. */ if (np->no_pps) np->time.status = SENSOR_S_CRIT; } /* Decode the GPS fix data for altitude. * - field 9 is the altitude in meters * $GNGGA,085901.00,1234.5678,N,00987.12345,E,1,12,0.84,1040.9,M,47.4,M,,*4B */ void nmea_decode_gga(struct nmea *np, struct tty *tp, char *fld[], int fldcnt) { if (fldcnt != 15) { DPRINTF(("GGA: field count mismatch, %d\n", fldcnt)); return; } #ifdef NMEA_DEBUG if (nmeadebug > 0) { linesw[TTYDISC].l_rint('[', tp); linesw[TTYDISC].l_rint('C', tp); linesw[TTYDISC].l_rint(']', tp); } #endif np->altitude.status = SENSOR_S_OK; if (nmea_atoi(&np->altitude.value, fld[9])) np->altitude.status = SENSOR_S_WARN; /* convert to uMeter */ np->altitude.value *= 1000; np->altitude.flags &= ~SENSOR_FINVALID; } /* * Convert nmea integer/decimal values in the form of XXXX.Y to an integer value * if it's a meter/altitude value, will be returned as mm */ int nmea_atoi(int64_t *dst, char *src) { char *p; int i = 3; /* take 3 digits */ *dst = 0; for (p = src; *p && *p != '.' && *p >= '0' && *p <= '9' ; ) *dst = *dst * 10 + (*p++ - '0'); /* *p should be '.' at that point */ if (*p != '.') return -1; /* no decimal point, or bogus value ? */ p++; /* read digits after decimal point, stop at first non-digit */ for (; *p && i > 0 && *p >= '0' && *p <= '9' ; i--) *dst = *dst * 10 + (*p++ - '0'); for (; i > 0 ; i--) *dst *= 10; DPRINTFN(2,("%s -> %lld\n", src, *dst)); return 0; } /* * Convert a nmea position in the form DDDMM.MMMM to an * angle sensor value (degrees*1000000) */ int nmea_degrees(int64_t *dst, char *src, int neg) { size_t ppos; int i, n; int64_t deg = 0, min = 0; char *p; while (*src == '0') ++src; /* skip leading zeroes */ for (p = src, ppos = 0; *p; ppos++) if (*p++ == '.') break; if (*p == '\0') return (-1); /* no decimal point */ for (n = 0; *src && n + 2 < ppos; n++) deg = deg * 10 + (*src++ - '0'); for (; *src && n < ppos; n++) min = min * 10 + (*src++ - '0'); src++; /* skip decimal point */ for (; *src && n < (ppos + 4); n++) min = min * 10 + (*src++ - '0'); for (i=0; i < 6 + ppos - n; i++) min *= 10; deg = deg * 1000000 + (min/60); *dst = neg ? -deg : deg; return (0); } /* * Convert a NMEA 0183 formatted date string to seconds since the epoch. * The string must be of the form DDMMYY. * Return 0 on success, -1 if illegal characters are encountered. */ int nmea_date_to_nano(char *s, int64_t *nano) { struct clock_ymdhms ymd; time_t secs; char *p; int n; /* make sure the input contains only numbers and is six digits long */ for (n = 0, p = s; n < 6 && *p && *p >= '0' && *p <= '9'; n++, p++) ; if (n != 6 || (*p != '\0')) return (-1); ymd.dt_year = 2000 + (s[4] - '0') * 10 + (s[5] - '0'); ymd.dt_mon = (s[2] - '0') * 10 + (s[3] - '0'); ymd.dt_day = (s[0] - '0') * 10 + (s[1] - '0'); ymd.dt_hour = ymd.dt_min = ymd.dt_sec = 0; secs = clock_ymdhms_to_secs(&ymd); *nano = secs * 1000000000LL; return (0); } /* * Convert NMEA 0183 formatted time string to nanoseconds since midnight. * The string must be of the form HHMMSS[.[sss]] (e.g. 143724 or 143723.615). * Return 0 on success, -1 if illegal characters are encountered. */ int nmea_time_to_nano(char *s, int64_t *nano) { long fac = 36000L, div = 6L, secs = 0L, frac = 0L; char ul = '2'; int n; for (n = 0, secs = 0; fac && *s && *s >= '0' && *s <= ul; s++, n++) { secs += (*s - '0') * fac; div = 16 - div; fac /= div; switch (n) { case 0: if (*s <= '1') ul = '9'; else ul = '3'; break; case 1: case 3: ul = '5'; break; case 2: case 4: ul = '9'; break; } } if (fac) return (-1); /* Handle the fractions of a second, up to a maximum of 6 digits. */ div = 1L; if (*s == '.') { for (++s; div < 1000000 && *s && *s >= '0' && *s <= '9'; s++) { frac *= 10; frac += (*s - '0'); div *= 10; } } if (*s != '\0') return (-1); *nano = secs * 1000000000LL + (int64_t)frac * (1000000000 / div); return (0); } /* * Degrade the sensor state if we received no NMEA sentences for more than * TRUSTTIME seconds. */ void nmea_timeout(void *xnp) { struct nmea *np = xnp; np->signal.value = 0; np->signal.status = SENSOR_S_CRIT; if (np->time.status == SENSOR_S_OK) { np->time.status = SENSOR_S_WARN; np->latitude.status = SENSOR_S_WARN; np->longitude.status = SENSOR_S_WARN; np->altitude.status = SENSOR_S_WARN; np->speed.status = SENSOR_S_WARN; /* * further degrade in TRUSTTIME seconds if no new valid NMEA * sentences are received. */ timeout_add_sec(&np->nmea_tout, TRUSTTIME); } else { np->time.status = SENSOR_S_CRIT; np->latitude.status = SENSOR_S_CRIT; np->longitude.status = SENSOR_S_CRIT; np->altitude.status = SENSOR_S_CRIT; np->speed.status = SENSOR_S_CRIT; } }