/* 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, 1997 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.5 1998/02/10 10:55:50 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>
#include	<sys/ioctl.h>
#include	<termios.h>

static	const char	id[] = "$Id: enable_pps.c,v 1.5 1998/02/10 10:55:50 windl Exp $";

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

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

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

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 */
static	void	pps_status(int retval, int status)
{
	struct ppsclockev	event;
	static	struct ppsclockev	last;
	static	char const	tag[] = "PPS: ";
	char const	*state_str = NULL;
	char const	*sep	= tag;

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

		if ( event.tv.tv_usec - last.tv.tv_usec < -500000 )
			--secs;
		else if ( event.tv.tv_usec - last.tv.tv_usec > 500000 )
			++secs;
		fprintf(stderr, "%spulse %u at 0.%06d",
			sep, event.serial, event.tv.tv_usec), sep = ", ";
		if ( secs != evs )
			fprintf(stderr, "%s%ld events in %ld seconds",
				sep, evs, secs), sep = ", ";
		last = event;
	}
	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);
	if ( status != 0 )
	{
		fprintf(stderr, "%s", sep), sep = ", ";
		write_status_bits(stderr, status);
	}
	if ( sep != tag )
		fprintf(stderr, "\n");
}

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

/* update kernel status bits with status check */
static	int	update_status(struct timex *txp, 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(txp) );
}

/* reset kernel PLL */
void	restart(struct timex *txp)
{
	txp->modes = MOD_FREQUENCY | MOD_ESTERROR | MOD_MAXERROR;
	update_status(txp, STA_PPSFREQ | STA_PPSTIME | STA_FLL, STA_PLL);
}

/* do offset adjustments. expects offset within bounds */
static	void	pll_adj_offset(struct timex *txp)
{
	static	time_t last_adj_time;
	time_t	t = time(NULL);
	long	offset = txp->offset;

	txp->modes = MOD_MAXERROR | MOD_ESTERROR;
	txp->esterror = txp->maxerror = txp->offset >= 0 ? : -txp->offset;
	if ( txp->offset > MAXPHASE )
		txp->offset -= 1000000;		/* make it negative */
	txp->offset = -txp->offset;	/* correct direction */
	if ( offset > MAXPHASE / 2 && offset < -MAXPHASE / 2 )
		txp->modes |= ADJ_OFFSET_SINGLESHOT;
	else if ( (txp->status & STA_FLL) != 0 && t - last_adj_time < MINSEC )
		return;			/* don't adjust too frequently */
	else
	{
		last_adj_time = t;
		txp->modes |= MOD_OFFSET;
	}
	verbose_adjtimex(txp);
}

/* 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;
	int		time_state;
	int		state;
	int		ch;
	int		keep_settings = 0;	/* keep kernel settings */
	int		monitor = 1;		/* do monitoring loop */

	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;
	verbose_adjtimex(&tx);
	print_stats(&tx);
	if ( !keep_settings )
	{
		restart(&tx);
		print_stats(&tx);
	}
	state = S_NO_PPS;
	while ( monitor )
	{
		if ( (time_state = verbose_adjtimex(&tx)) == 0 )
			pps_status(time_state, tx.status);
		print_stats(&tx);
		switch ( state )
		{
		case S_NO_PPS:	/* wait for good PPS */
			restart(&tx);
			if ( time_state != 0 )
				break;
			else if ( (tx.status & STA_PPSSIGNAL) != 0
				  && tx.stabil < MAXFREQ )
			{
  				state = S_PPS_FREQ;
				status("valid PPS signal");
			}
			if ( (tx.status & STA_PPSSIGNAL) != 0 )
			{
				pll_adj_offset(&tx);
			}
			break;
		case S_PPS_FREQ:	/* adjust PLL frequency from PPS */
			update_status(&tx,
				      STA_PPSTIME | STA_FLL | STA_FREQHOLD,
				      STA_PPSFREQ | STA_PLL);
			if ( time_state != 0 )
			{
				status("bad PPS signal");
				state = S_NO_PPS;
			}
			else if ( tx.stabil < (10 << SHIFT_USEC) )
			{
				status("PPS frequency stabilized");
				state = S_PPS_TIME;
			}
			break;
		case S_PPS_TIME:	/* adjust the offset */
			update_status(&tx, STA_FLL,
				      STA_PPSTIME | STA_PPSFREQ | STA_PLL);
			if ( time_state != 0 )
			{
				if ( tx.stabil > (50 << SHIFT_USEC) )
				{
					status("PPS lost stability");
					state = S_PPS_FREQ;
				}
			}
			break;
		}
		sleep(1);
	}
	return(0);
}
