/* Sample program to test the PPS-related kernel routines
 * (enable PPS detection, watch for errors, and monitor data)
 *
 * Errors and exceptional messages go to stderr, while monitoring data
 * is sent to stdout.
 *
 * A detailed description of the clock model can be found in the technical
 * memorandum "A Kernel Model for Precision Timekeeping" by Dave Mills,
 * revised 31 January 1996. That document updates RFC1589.
 *
 * Copyright (c) 1996 - 1999 by Ulrich Windl
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: enable_pps.c,v 1.13 1999/01/14 21:11:40 windl Exp $ */
#include	<unistd.h>
#include	<time.h>
#include	<stdlib.h>
#include	<stdio.h>
#include	<string.h>
#include	<errno.h>
#include	<sys/timex.h>

#include	<sys/ppsclock.h>
#include	<linux/serial.h>
#if defined(__GNU_LIBRARY__) && __GNU_LIBRARY__ >= 6	/* libc6 or glibc2 */
# warning	"Compatibility with this library has not been tested a lot!"
# include	<ioctl-types.h>
#else
# include	<sys/ioctl.h>
				/* conflict: ioctl-types.h vs. some
				 * linux file
				 */
# include	<termios.h>	/* conflict: termbits.h vs. asm/termbits.h
				 * struct termios differs in size (NCCS)
				 */
#endif

#define OFFSET_TOLERANCE	50000
#define STABIL_TOLERANCE	(10 << SHIFT_USEC)
#define	PPS_INSANE		(STA_PPSERROR|STA_PPSWANDER|STA_PPSJITTER)

#define	ALL_BITS_ON(v, b)		(((v) & (b)) == (b))
#define	ALL_BITS_OFF(v, b)		(((v) & (b)) == 0)

static	const char	id[] = "$Id: enable_pps.c,v 1.13 1999/01/14 21:11:40 windl Exp $";

static	char	polarity	= 'p';	/* p[ositive] | n[egative] | 0) */

static	const	int	fd	= 0;	/* file descriptor of port (stdin) */

static	void	print_header(void)
{
	printf("%4.4s %7.7s %7.7s %7.7s %4.4s %1.1s "
	       "%7.7s %4.4s %4.4s %4.4s %4.4s\n",
	       "Status", "Offset",
	       "Frequency",
	       "PPS Frequency",
	       "Jitter", "Shift",
	       "stabil",
	       "jitcnt", "calcnt",
	       "errcnt", "stbcnt");
}

static	void	print_stats(const struct timex *tp)
{
	printf("%04x %7ld %7.3f %7.3f %4ld %1d %7.3f %4ld %4ld %4ld %4ld\n",
	       tp->status, tp->offset,
	       (double) tp->freq / (1L << SHIFT_USEC),
	       (double) tp->ppsfreq / (1L << SHIFT_USEC),
	       tp->jitter, tp->shift,
	       (double) tp->stabil / (1L << SHIFT_USEC),
	       tp->jitcnt, tp->calcnt,
	       tp->errcnt, tp->stbcnt);
	fflush(stdout);
}

enum pps_state { S_NO_PPS, S_PPS_TIME, S_PPS_FREQ, S_PPS_FLL };

static	char const	*status_str[] = {	/* from <timex.h> */
	"PLL",		/* bit 0 */
	"PPSFREQ",	/* bit 1 */
	"PPSTIME",	/* bit 2 */
	"FLL",		/* bit 3 */

	"INS",		/* bit 4 */
	"DEL",		/* bit 5 */
	"UNSYNC",	/* bit 6 */
	"FREQHOLD",	/* bit 7 */

	"PPSSIGNAL",	/* bit 8 */
	"PPSJITTER",	/* bit 9 */
	"PPSWANDER",	/* bit 10 */
	"PPSERROR",	/* bit 11 */

	"CLOCKERR",	/* bit 12 */
	"???13",	/* bit 13 */
	"???14",	/* bit 14 */
	"???15",	/* bit 15 */
};

/* write strings for set status bits on ``fp'' */
static	void	write_status_bits(FILE *fp, int bits)
{
	int	b;
	char	*sep = "";
	for ( b = 0; b < 16; ++b )
	{
		if ( (bits & 1) != 0 )
			fprintf(fp, "%s%s", sep, status_str[b]), sep = "+";
		bits >>= 1;
	}
}

/* print PPS status and update *evp */
static	int	pps_status(const char *tag, const int show_event,
			   int retval, int status, struct ppsclockev *evp)
{
	static	struct ppsclockev	last;
	int	result = -1;
	char const	*state_str = NULL;
	char const	*sep	= tag;

	if ( ioctl(fd, CIOGETEV, evp) == -1 )
	{
		perror("ioctl(CIOGETEV)");
		memset(evp, 0, sizeof(*evp));
	}
	
	if ( show_event && evp->serial != last.serial )
	{
		long	secs	= evp->tv.tv_sec - last.tv.tv_sec;
		long	evs	= evp->serial - last.serial;

		if ( evp->tv.tv_usec - last.tv.tv_usec < -500000 )
			--secs;
		else if ( evp->tv.tv_usec - last.tv.tv_usec > 500000 )
			++secs;
		fprintf(stderr, "%spulse %u at 0.%06d",
			sep, evp->serial, evp->tv.tv_usec), sep = ", ";
		if ( secs != evs )
			fprintf(stderr, "%s%ld events in %ld seconds",
				sep, evs, secs), sep = ", ";
		last = *evp;
		result = 0;
	}
	switch ( retval )
	{
	case TIME_INS: state_str = "TIME_INS"; break;
	case TIME_DEL: state_str = "TIME_DEL"; break;
	case TIME_OOP: state_str = "TIME_OOP"; break;
	case TIME_WAIT: state_str = "TIME_WAIT"; break;
	case TIME_ERROR: state_str = "TIME_ERROR"; break;
	}
	if ( state_str != NULL )
		fprintf(stderr, "%sstate=%s", sep, state_str), sep = ", ";

	if ( !(status & STA_PPSSIGNAL) )
		fprintf(stderr, "%sno pulse", sep), sep = ", ";
	status &= (STA_PPSJITTER|STA_PPSERROR|STA_PPSWANDER|STA_CLOCKERR);
	if ( status != 0 )
	{
		fprintf(stderr, "%serrors=", sep), sep = ", ";
		write_status_bits(stderr, status);
	}
	if ( sep != tag )
		fprintf(stderr, "\n");
	return(result);
}

/* do adjtimex and check for errors */
static	int	verbose_adjtimex(const char *tag, const int show_event,
				 struct timex *txp, struct ppsclockev *evp)
{
	int	state = adjtimex(txp);
	if ( state > 0 )
	{
		pps_status(tag, show_event, state, txp->status, evp);
	}
	else if ( state < 0 )
	{
		errno = -state;
		perror("adjtimex()");
	}
	return(state);
}

/* update kernel status bits with status check */
static	int	update_status(struct timex *txp, struct ppsclockev *evp,
			      int clear_bits, int set_bits)
{
	int	old_status = txp->status;
	int	change;
	char	*sep = " (";

	txp->status |= set_bits;
	txp->status &= ~clear_bits;
	txp->modes = MOD_STATUS;
	if ( txp->status == old_status )
		return(0);
	fprintf(stderr, "update status: %04x -> %04x",
		old_status, txp->status);
	if ( (change = ~old_status & (old_status ^ txp->status)) != 0 )
	{
		fprintf(stderr, "%sset=[", sep), sep = ", ";
		write_status_bits(stderr, change);
		fprintf(stderr, "]");
	}
	if ( (change = old_status & (old_status ^ txp->status)) != 0 )
	{
		fprintf(stderr, "%sclear=[", sep), sep = ", ";
		write_status_bits(stderr, change);
		fprintf(stderr, "]");
	}
	fprintf(stderr, "%s\n", sep[0] == ',' ? ")" : "");
	return( verbose_adjtimex("update_status(): ", 0, txp, evp) );
}

/* reset kernel PLL */
static	void	restart(struct timex *txp, struct ppsclockev *evp)
{
	txp->modes = MOD_ESTERROR | MOD_MAXERROR;
	/* indicate that we are trying to synchronize */
	txp->esterror = txp->maxerror = MAXPHASE / 1000;
	txp->modes |= MOD_FREQUENCY | ADJ_TICKADJ;
	txp->freq = 0;
	/* boost adjtime() times 100 (HZ*500 == 0.05s per second) */
	txp->tickadj = 500;			
	txp->modes |= MOD_TIMECONST;
	txp->constant = 1;
	verbose_adjtimex("restart(): ", 0, txp, evp);
	update_status(txp, evp, STA_FLL | STA_PPSFREQ | STA_PPSTIME,
		      STA_FREQHOLD | STA_PLL | STA_UNSYNC);
}

/* use current time constant from PPS discipline if it is working properly */
static	void	update_constant(struct timex *txp, struct ppsclockev *evp)
{
	if ( (txp->status & STA_PPSSIGNAL) != 0 &&
	     (txp->status & PPS_INSANE) == 0 )
		txp->constant = txp->shift;		/* copy PPS PLL constant */
	else
		txp->constant = 1;
	txp->modes = MOD_TIMECONST;
	verbose_adjtimex("update_constant(): ", 0, txp, evp);
}

/* do offset adjustments. expects offset within bounds */
static	void	pll_adj_offset(struct timex *txp, struct ppsclockev *evp)
{
	static	time_t last_adj_time;
	static	u_int  last_serial;
	time_t	t = time(NULL);
	long	offset = evp->tv.tv_usec;
	long	tdelta, tconst;

	if ( last_serial == evp->serial )
		return;
	last_serial = evp->serial;
	if ( offset > 500000 )
		offset -= 1000000;	/* change direction */
	offset = -offset;		/* correct direction */
	txp->modes = MOD_MAXERROR | MOD_ESTERROR;
	txp->esterror = txp->maxerror = offset >= 0 ? : -offset;
	txp->offset = (txp->offset + offset) / 2;
	if ( txp->offset > MAXPHASE / 2 || txp->offset < -MAXPHASE / 2 )
		txp->modes = ADJ_OFFSET_SINGLESHOT;
	else
		txp->modes |= MOD_OFFSET;
	tdelta = t - last_adj_time;
	tconst = ALL_BITS_ON(txp->status, STA_PPSTIME|STA_PPSSIGNAL) ?
		txp->shift : txp->constant;
	if ( ((txp->status & (STA_FLL|STA_PPSFREQ)) != 0 &&
	      (tdelta < MINSEC || tdelta < (1L << tconst))) ||
	     ((txp->status & STA_PLL) != 0 && tdelta < (1L << tconst) / 2) )
		return;			/* don't adjust too frequently */
	else
		last_adj_time = t;
	printf("%s: modes=%04x, delta=%ld, const=%ld, in=%ld, out=%ld\n",
	       "pll_adj_offset", txp->modes, tdelta, tconst, offset,
	       txp->offset);
	verbose_adjtimex("pll_adj_offset(): ", 0, txp, evp);
}

/* adjust offset if conditions are sane */
static	void	update_offset(struct timex *txp, struct ppsclockev *evp)
{
	if ( ((txp->status & STA_PPSSIGNAL) != 0 &&
	      (txp->status & PPS_INSANE) == 0) ||
	     (txp->status & STA_PPSSIGNAL) == 0 )
		pll_adj_offset(txp, evp);	/* uses pps_offset! */
}

/* print status message */
static	void	status(char const *str)
{
	fprintf(stderr, "[%s]\n", str);
}

/* provide a usage message */
static	void	usage(void)
{
	fprintf(stderr,
		"Known options are:\n"
		"\t-e\texit - do not enter monitoring loop\n"
		"\t-k\tkeep settings (when entering monitoring loop)\n"
		"\t-p{p|0|n}\tpolarity (of pulse) positive|none|negative\n"
		);
}

/* input has to be redirected to the desired device! */
int	main(int argc, char *argv[])
{
	int		stat = TIOCM_RTS;	/* RTS MODEM control line */
	struct serial_struct ss;
	struct timex 	tx;
	struct ppsclockev event;
	int		time_state;
	int		state;
	int		ch;
	int		keep_settings = 0;	/* keep kernel settings */
	int		monitor = 1;		/* do monitoring loop */

	fprintf(stderr,
		"WARNING: This program may invalidate your system time!\n");
	while ( (ch = getopt(argc, argv, "ekp:")) != -1 )
	{
		switch ( ch )
		{
		case 'k':	/* keep settings - don't restart when entering
				   monitoring loop */
			keep_settings = 1;
			break;
		case 'e':	/* exit - don't enter monitoring loop */
			monitor = 0;
			break;
		case 'p':	/* select desired edge of the pulse */
			polarity = optarg[0];
			switch ( polarity )
			{
			case 'p':
			case 'n':
			case 'm':
			case '0':
				break;
			default:
				fprintf(stderr, "polarity '%c' is illegal\n",
					polarity);
				polarity = '0';
			}
			break;
		case '?':
			fprintf (stderr, "Unknown option `-%c'.\n", optopt);
			usage();
			exit(1);
		}
	}
	if ( optind > argc )
	{
		fprintf(stderr, "Extra arguments ignored!\n");
		usage();
		exit(1);
	}

	ioctl(fd, TIOCMBIC, &stat);	/* power-on DCF77 receiver */
	ioctl(fd, TIOCGSERIAL, &ss);	/* enable pulse detection on DCD */
	switch ( polarity )
	{
	case '0':
		ss.flags &= ~(ASYNC_PPS_CD_POS | ASYNC_PPS_CD_NEG); break;
	case 'p':
		ss.flags &= ~ASYNC_PPS_CD_NEG;
		ss.flags |= ASYNC_PPS_CD_POS;
		break;
	default:
		ss.flags &= ~ASYNC_PPS_CD_POS;
		ss.flags |= ASYNC_PPS_CD_NEG;
	}
	if ( ioctl(fd, TIOCSSERIAL, &ss) != 0 )
		perror("ioctl(TIOCSSERIAL) to enable PPS detection");

	fprintf(stderr,
		"PPS detection enabled on CD-pin for polarity '%c'\n",
		polarity);
	tx.modes = 0;
	print_header();
	verbose_adjtimex("init: ", 1, &tx, &event);
	print_stats(&tx);
	if ( !keep_settings )
	{
		restart(&tx, &event);
		print_stats(&tx);
	}
	state = S_NO_PPS;
	while ( monitor )
	{
		tx.modes = 0;
		time_state = verbose_adjtimex("monitor: ", 1, &tx, &event);
		print_stats(&tx);
		switch ( state )
		{
		case S_NO_PPS:	/* wait for good PPS */
			update_status(&tx, &event,
				      STA_PPSTIME | STA_PPSFREQ | STA_FLL,
				      STA_PLL | STA_FREQHOLD);
			update_offset(&tx, &event);
			if ( (tx.status & STA_PPSSIGNAL) != 0
			     && (tx.status & PPS_INSANE) == 0
			     && (tx.offset > -OFFSET_TOLERANCE
				 && tx.offset < OFFSET_TOLERANCE) )
			{
  				state = S_PPS_TIME;
				status("S_NO_PPS: PPS detected and in range");
			}
			break;
		case S_PPS_TIME:	/* adjust the offset */
			update_status(&tx, &event,
				      STA_FLL | STA_PPSFREQ,
				      STA_PPSTIME | STA_FREQHOLD | STA_PLL);
			update_offset(&tx, &event);
			if ( (event.tv.tv_usec > 2 * OFFSET_TOLERANCE &&
			      event.tv.tv_usec <
			      1000000 - 2 * OFFSET_TOLERANCE) )
			{
				status("S_PPS_TIME: PPS got insane");
				state = S_NO_PPS;
			}
			else if ( (tx.status & STA_PPSSIGNAL) != 0 &&
				  (tx.status & PPS_INSANE) == 0 &&
				  tx.offset > -OFFSET_TOLERANCE / 2 &&
				  tx.offset <= OFFSET_TOLERANCE / 2 &&
				  tx.stabil <= STABIL_TOLERANCE &&
				  tx.shift > PPS_SHIFT )
			{
  				state = S_PPS_FREQ;
				status("S_PPS_TIME: PPS offset is low and stable");
			}
			break;
		case S_PPS_FREQ:	/* adjust PLL frequency from PPS */
			update_status(&tx, &event,
				      STA_FLL | STA_FREQHOLD,
				      STA_PPSTIME | STA_PPSFREQ | STA_PLL);
			update_constant(&tx, &event);
			update_offset(&tx, &event);
			if ( tx.shift == PPS_SHIFT ||
			     tx.offset < -OFFSET_TOLERANCE ||
			     tx.offset >= OFFSET_TOLERANCE ||
			     (tx.status & STA_PPSSIGNAL) == 0 ||
			     (tx.status & ~STA_PPSJITTER & PPS_INSANE) != 0 )
			{
				state = S_NO_PPS;
				status("S_PPS_FREQ: insane PPS signal");
			}
			else if ( tx.shift == PPS_SHIFTMAX &&
				  tx.offset > -OFFSET_TOLERANCE / 4 &&
				  tx.offset <= OFFSET_TOLERANCE / 4 &&
				  tx.stabil <= STABIL_TOLERANCE / 2 )
			{
				status("PPS_FREQ: best quality PPS signal");
				state = S_PPS_FLL;
			}
			break;
		case S_PPS_FLL:	/* adjust PLL frequency from PPS */
			update_status(&tx, &event,
				      STA_FREQHOLD,
				      STA_PPSTIME | STA_PPSFREQ | STA_FLL |
				      STA_PLL);
			update_constant(&tx, &event);
			if ( (tx.status & (STA_PPSSIGNAL)) == 0 ||
			     (tx.status & ~STA_PPSJITTER & PPS_INSANE) != 0 ||
			     tx.stabil > STABIL_TOLERANCE ||
 			     tx.shift < PPS_SHIFTMAX )
			{
				state = S_PPS_FREQ;
				status("S_PPS_FLL: PPS signal lost quality");
			}
			break;
		}
		sleep(1);
	}
	return(0);
}
