[BACK]Return to tty_nmea.c CVS log [TXT][DIR] Up to [local] / src / sys / kern

File: [local] / src / sys / kern / tty_nmea.c (download)

Revision 1.51, Sat Apr 2 22:45:18 2022 UTC (2 years, 2 months ago) by mlarkin
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, OPENBSD_7_3_BASE, OPENBSD_7_3, OPENBSD_7_2_BASE, OPENBSD_7_2, OPENBSD_7_1_BASE, OPENBSD_7_1, HEAD
Changes since 1.50: +5 -2 lines

Update an old comment

The old comment only mentioned that tty_nmea was used for time, but
subsequently position data was added to this line discipline.

/*	$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;
	}
}