root/sys/kern/tty_nmea.c
/*      $OpenBSD: tty_nmea.c,v 1.51 2022/04/02 22:45:18 mlarkin Exp $ */

/*
 * Copyright (c) 2006, 2007, 2008 Marc Balmer <mbalmer@openbsd.org>
 *
 * 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 <sys/param.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/sensors.h>
#include <sys/tty.h>
#include <sys/conf.h>
#include <sys/time.h>

#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;
        }
}