/*	$OpenBSD: tty_endrun.c,v 1.6 2014/11/03 03:08:00 deraadt Exp $ */

/*
 * Copyright (c) 2008 Marc Balmer <mbalmer@openbsd.org>
 * Copyright (c) 2009 Kevin Steves <stevesk@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 the EndRun Technologies native
 * time-of-day message.
 * http://www.endruntechnologies.com/
 */

/*
 * EndRun Format:
 *
 * T YYYY DDD HH:MM:SS zZZ m<CR><LF>
 *
 * T is the Time Figure of Merit (TFOM) character (described below).
 * This is the on-time character, transmitted during the first
 * millisecond of each second.
 *
 * YYYY is the year
 * DDD is the day-of-year
 * : is the colon character (0x3A)
 * HH is the hour of the day
 * MM is the minute of the hour
 * SS is the second of the minute
 * z is the sign of the offset to UTC, + implies time is ahead of UTC.
 * ZZ is the magnitude of the offset to UTC in units of half-hours.
 * Non-zero only when the Timemode is Local.
 * m is the Timemode character and is one of:
 *   G = GPS
 *   L = Local
 *   U = UTC
 * <CR> is the ASCII carriage return character (0x0D)
 * <LF> is the ASCII line feed character (0x0A)
 */

#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 ENDRUN_DEBUG
#define DPRINTFN(n, x)	do { if (endrundebug > (n)) printf x; } while (0)
int endrundebug = 0;
#else
#define DPRINTFN(n, x)
#endif
#define DPRINTF(x)	DPRINTFN(0, x)

int	endrunopen(dev_t, struct tty *, struct proc *p);
int	endrunclose(struct tty *, int, struct proc *p);
int	endruninput(int, struct tty *);
void	endrunattach(int);

#define ENDRUNLEN	27 /* strlen("6 2009 018 20:41:17 +00 U\r\n") */
#define NUMFLDS		6
#ifdef ENDRUN_DEBUG
#define TRUSTTIME	30
#else
#define TRUSTTIME	(10 * 60)	/* 10 minutes */
#endif

int endrun_count, endrun_nxid;

struct endrun {
	char			cbuf[ENDRUNLEN];	/* receive buffer */
	struct ksensor		time;		/* the timedelta sensor */
	struct ksensor		signal;		/* signal status */
	struct ksensordev	timedev;
	struct timespec		ts;		/* current timestamp */
	struct timespec		lts;		/* timestamp of last TFOM */
	struct timeout		endrun_tout;	/* invalidate sensor */
	int64_t			gap;		/* gap between two sentences */
	int64_t			last;		/* last time rcvd */
#define SYNC_SCAN	1	/* scanning for '\n' */
#define SYNC_EOL	2	/* '\n' seen, next char TFOM */
	int			sync;
	int			pos;		/* position in rcv buffer */
	int			no_pps;		/* no PPS although requested */
#ifdef ENDRUN_DEBUG
	char			tfom;
#endif
};

/* EndRun decoding */
void	endrun_scan(struct endrun *, struct tty *);
void	endrun_decode(struct endrun *, struct tty *, char *fld[], int fldcnt);

/* date and time conversion */
int	endrun_atoi(char *s, int len);
int	endrun_date_to_nano(char *s1, char *s2, int64_t *nano);
int	endrun_time_to_nano(char *s, int64_t *nano);
int	endrun_offset_to_nano(char *s, int64_t *nano);

/* degrade the timedelta sensor */
void	endrun_timeout(void *);

void
endrunattach(int dummy)
{
}

int
endrunopen(dev_t dev, struct tty *tp, struct proc *p)
{
	struct endrun *np;
	int error;

	DPRINTF(("endrunopen\n"));
	if (tp->t_line == ENDRUNDISC)
		return ENODEV;
	if ((error = suser(p, 0)) != 0)
		return error;
	np = malloc(sizeof(struct endrun), M_DEVBUF, M_WAITOK|M_ZERO);
	snprintf(np->timedev.xname, sizeof(np->timedev.xname), "endrun%d",
	    endrun_nxid++);
	endrun_count++;
	np->time.status = SENSOR_S_UNKNOWN;
	np->time.type = SENSOR_TIMEDELTA;
#ifndef ENDRUN_DEBUG
	np->time.flags = SENSOR_FINVALID;
#endif
	sensor_attach(&np->timedev, &np->time);

	np->signal.type = SENSOR_PERCENT;
	np->signal.status = SENSOR_S_UNKNOWN;
	np->signal.value = 100000LL;
	strlcpy(np->signal.desc, "Signal", sizeof(np->signal.desc));
	sensor_attach(&np->timedev, &np->signal);

	np->sync = SYNC_SCAN;
#ifdef ENDRUN_DEBUG
	np->tfom = '0';
#endif
	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->endrun_tout, endrun_timeout, np);
	}

	return error;
}

int
endrunclose(struct tty *tp, int flags, struct proc *p)
{
	struct endrun *np = (struct endrun *)tp->t_sc;

	DPRINTF(("endrunclose\n"));
	tp->t_line = TTYDISC;	/* switch back to termios */
	timeout_del(&np->endrun_tout);
	sensordev_deinstall(&np->timedev);
	free(np, M_DEVBUF, sizeof(*np));
	tp->t_sc = NULL;
	endrun_count--;
	if (endrun_count == 0)
		endrun_nxid = 0;
	return linesw[TTYDISC].l_close(tp, flags, p);
}

/* collect EndRun sentence from tty */
int
endruninput(int c, struct tty *tp)
{
	struct endrun *np = (struct endrun *)tp->t_sc;
	struct timespec ts;
	int64_t gap;
	long tmin, tmax;

	if (np->sync == SYNC_EOL) {
		nanotime(&ts);
		np->pos = 0;
		np->sync = SYNC_SCAN;
		np->cbuf[np->pos++] = c; /* TFOM char */

		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)
			goto nogap;

		np->ts.tv_sec = ts.tv_sec;
		np->ts.tv_nsec = ts.tv_nsec;
		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 EndRun 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;
			}
		}
	} else if (c == '\n') {
		if (np->pos == ENDRUNLEN - 1) {
			/* don't copy '\n' into cbuf */
			np->cbuf[np->pos] = '\0';
			endrun_scan(np, tp);
		}
		np->sync = SYNC_EOL;
	} else {
		if (np->pos < ENDRUNLEN - 1)
			np->cbuf[np->pos++] = c;
	}

nogap:
	/* pass data to termios */
	return linesw[TTYDISC].l_rint(c, tp);
}

/* Scan the EndRun sentence just received */
void
endrun_scan(struct endrun *np, struct tty *tp)
{
	int fldcnt = 0, n;
	char *fld[NUMFLDS], *cs;

	DPRINTFN(1, ("%s\n", np->cbuf));
	/* split into fields */
	fld[fldcnt++] = &np->cbuf[0];
	for (cs = NULL, n = 0; n < np->pos && cs == NULL; n++) {
		switch (np->cbuf[n]) {
		case '\r':
			np->cbuf[n] = '\0';
			cs = &np->cbuf[n + 1];
			break;
		case ' ':
			if (fldcnt < NUMFLDS) {
				np->cbuf[n] = '\0';
				fld[fldcnt++] = &np->cbuf[n + 1];
			} else {
				DPRINTF(("endrun: nr of fields in sentence "
				    "exceeds expected: %d\n", NUMFLDS));
				return;
			}
			break;
		}
	}
	endrun_decode(np, tp, fld, fldcnt);
}

/* Decode the time string */
void
endrun_decode(struct endrun *np, struct tty *tp, char *fld[], int fldcnt)
{
	int64_t date_nano, time_nano, offset_nano, endrun_now;
	char tfom;
	int jumped = 0;

	if (fldcnt != NUMFLDS) {
		DPRINTF(("endrun: field count mismatch, %d\n", fldcnt));
		return;
	}
	if (endrun_time_to_nano(fld[3], &time_nano) == -1) {
		DPRINTF(("endrun: illegal time, %s\n", fld[3]));
		return;
	}
	if (endrun_date_to_nano(fld[1], fld[2], &date_nano) == -1) {
		DPRINTF(("endrun: illegal date, %s %s\n", fld[1], fld[2]));
		return;
	}
	offset_nano = 0;
	/* only parse offset when timemode is local */
	if (fld[5][0] == 'L' &&
	    endrun_offset_to_nano(fld[4], &offset_nano) == -1) {
		DPRINTF(("endrun: illegal offset, %s\n", fld[4]));
		return;
	}

	endrun_now = date_nano + time_nano + offset_nano;
	if (endrun_now <= np->last) {
		DPRINTF(("endrun: time not monotonically increasing "
		    "last %lld now %lld\n",
		    (long long)np->last, (long long)endrun_now));
		jumped = 1;
	}
	np->last = endrun_now;
	np->gap = 0LL;
#ifdef ENDRUN_DEBUG
	if (np->time.status == SENSOR_S_UNKNOWN) {
		np->time.status = SENSOR_S_OK;
		timeout_add_sec(&np->endrun_tout, TRUSTTIME);
	}
#endif

	np->time.value = np->ts.tv_sec * 1000000000LL +
	    np->ts.tv_nsec - endrun_now;
	np->time.tv.tv_sec = np->ts.tv_sec;
	np->time.tv.tv_usec = np->ts.tv_nsec / 1000L;
	if (np->time.status == SENSOR_S_UNKNOWN) {
		np->time.status = SENSOR_S_OK;
		np->time.flags &= ~SENSOR_FINVALID;
		strlcpy(np->time.desc, "EndRun", sizeof(np->time.desc));
	}
	/*
	 * Only update the timeout if the clock reports the time as valid.
	 *
	 * Time Figure Of Merit (TFOM) values:
	 *
	 * 6  - time error is < 100 us
	 * 7  - time error is < 1 ms
	 * 8  - time error is < 10 ms
	 * 9  - time error is > 10 ms,
	 *      unsynchronized state if never locked to CDMA
	 */

	switch (tfom = fld[0][0]) {
	case '6':
	case '7':
	case '8':
		np->time.status = SENSOR_S_OK;
		np->signal.status = SENSOR_S_OK;
		break;
	case '9':
		np->signal.status = SENSOR_S_WARN;
		break;
	default:
		DPRINTF(("endrun: invalid TFOM: '%c'\n", tfom));
		np->signal.status = SENSOR_S_CRIT;
		break;
	}

#ifdef ENDRUN_DEBUG
	if (np->tfom != tfom) {
		DPRINTF(("endrun: TFOM changed from %c to %c\n",
		    np->tfom, tfom));
		np->tfom = tfom;
	}
#endif
	if (jumped)
		np->time.status = SENSOR_S_WARN;
	if (np->time.status == SENSOR_S_OK)
		timeout_add_sec(&np->endrun_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;
}

int
endrun_atoi(char *s, int len)
{
	int n;
	char *p;

	/* make sure the input contains only numbers */
	for (n = 0, p = s; n < len && *p && *p >= '0' && *p <= '9'; n++, p++)
		;
	if (n != len || *p != '\0')
		return -1;

	for (n = 0; *s; s++)
		n = n * 10 + *s - '0';

	return n;
}

/*
 * Convert date fields from EndRun to nanoseconds since the epoch.
 * The year string must be of the form YYYY .
 * The day of year string must be of the form DDD .
 * Return 0 on success, -1 if illegal characters are encountered.
 */
int
endrun_date_to_nano(char *y, char *doy, int64_t *nano)
{
	struct clock_ymdhms clock;
	time_t secs;
	int n, i;
	int year_days = 365;
	int month_days[] = {
		0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
	};

#define FEBRUARY		2

#define LEAPYEAR(x)		\
	((x) % 4 == 0 &&	\
	(x) % 100 != 0) ||	\
	(x) % 400 == 0

	if ((n = endrun_atoi(y, 4)) == -1)
		return -1;
	clock.dt_year = n;

	if (LEAPYEAR(n)) {
		month_days[FEBRUARY]++;
		year_days++;
	}

	if ((n = endrun_atoi(doy, 3)) == -1 || n == 0 || n > year_days)
		return -1;

	/* convert day of year to month, day */
	for (i = 1; n > month_days[i]; i++) {
		n -= month_days[i];
	}
	clock.dt_mon = i;
	clock.dt_day = n;

	DPRINTFN(1, ("mm/dd %d/%d\n", i, n));

	clock.dt_hour = clock.dt_min = clock.dt_sec = 0;

	secs = clock_ymdhms_to_secs(&clock);
	*nano = secs * 1000000000LL;
	return 0;
}

/*
 * Convert time field from EndRun to nanoseconds since midnight.
 * The string must be of the form HH:MM:SS .
 * Return 0 on success, -1 if illegal characters are encountered.
 */
int
endrun_time_to_nano(char *s, int64_t *nano)
{
	struct clock_ymdhms clock;
	time_t secs;
	int n;

	if (s[2] != ':' || s[5] != ':')
		return -1;

	s[2] = '\0';
	s[5] = '\0';

	if ((n = endrun_atoi(&s[0], 2)) == -1 || n > 23)
		return -1;
	clock.dt_hour = n;
	if ((n = endrun_atoi(&s[3], 2)) == -1 || n > 59)
		return -1;
	clock.dt_min = n;
	if ((n = endrun_atoi(&s[6], 2)) == -1 || n > 60)
		return -1;
	clock.dt_sec = n;

	DPRINTFN(1, ("hh:mm:ss %d:%d:%d\n", (int)clock.dt_hour,
	    (int)clock.dt_min,
	    (int)clock.dt_sec));
	secs = clock.dt_hour * 3600
	    + clock.dt_min * 60
	    + clock.dt_sec;
	    
	DPRINTFN(1, ("secs %lu\n", (unsigned long)secs));

	*nano = secs * 1000000000LL;
	return 0;
}

int
endrun_offset_to_nano(char *s, int64_t *nano)
{
	time_t secs;
	int n;

	if (!(s[0] == '+' || s[0] == '-'))
		return -1;

	if ((n = endrun_atoi(&s[1], 2)) == -1)
		return -1;
	secs = n * 30 * 60;

	*nano = secs * 1000000000LL;
	if (s[0] == '+')
		*nano = -*nano;

	DPRINTFN(1, ("offset secs %lu nanosecs %lld\n",
	    (unsigned long)secs, (long long)*nano));

	return 0;
}

/*
 * Degrade the sensor state if we received no EndRun string for more than
 * TRUSTTIME seconds.
 */
void
endrun_timeout(void *xnp)
{
	struct endrun *np = xnp;

	if (np->time.status == SENSOR_S_OK) {
		np->time.status = SENSOR_S_WARN;
		/*
		 * further degrade in TRUSTTIME seconds if no new valid EndRun
		 * strings are received.
		 */
		timeout_add_sec(&np->endrun_tout, TRUSTTIME);
	} else
		np->time.status = SENSOR_S_CRIT;
}
