/*
 * refclock_atom - clock driver for 1-pps signals
 */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#if defined(REFCLOCK) && defined(ATOM)

#include <stdio.h>
#include <ctype.h>
#include <sys/time.h>

#include "ntpd.h"
#include "ntp_io.h"
#include "ntp_unixtime.h"
#include "ntp_refclock.h"
#include "ntp_stdlib.h"

#ifdef PPS
# include <sys/ppsclock.h>
#endif /* PPS */

/*
 * This driver furnishes an interface for pulse-per-second (PPS) signals
 * produced by a cesium clock, timing receiver or related  equipment. It
 * can be used to remove accumulated jitter and retime a secondary
 * server when synchronized to a primary server over a congested, wide-
 * area network and before redistributing the time to local clients.
 *
 * In order for this driver to work, the local clock must be set to
 * within +-500 ms by another means, such as a radio clock or NTP
 * itself. The 1-pps signal is connected via a serial port and gadget
 * box consisting of a one-shot and RS232 level converter. When operated
 * at 38.4 kbps with a SPARCstation IPC, this arrangement has a worst-
 * case jitter less than 26 us.
 *
 * There are three ways in which this driver can be used. The first way
 * uses the LDISC_PPS line discipline and works only for the baseboard
 * serial ports of the Sun SPARCstation. The PPS signal is connected via
 * a gadget box to the carrier detect (CD) line of a serial port and
 * flag3 of the driver configured for that port is set. This causes the
 * ppsclock streams module to be configured for that port and capture a
 * timestamp at the on-time transition of the PPS signal. This driver
 * then reads the timestamp directly by a designated ioctl() system
 * call. This provides the most accurate time and least jitter of any
 * other scheme. There is no need to configure a dedicated device for
 * this purpose, which ordinarily is the device used for the associated
 * radio clock.
 *
 * The second way uses the LDISC_CLKPPS line discipline and works for
 * any architecture supporting a serial port. If after a few seconds
 * this driver finds no ppsclock module configured, it attempts to open
 * a serial port device /dev/pps%d, where %d is the unit number, and
 * assign the LDISC_CLKPPS line discipline to it. If the line discipline
 * fails, no harm is done except the accuracy is reduced somewhat. The
 * pulse generator in the gadget box is adjusted to produce a start bit
 * of length 26 usec at 38400 bps. Used with the LDISC_CLKPPS line
 * discipline, this produces an ASCII DEL character ('\377') followed by
 * a timestamp at each seconds epoch. 
 *
 * The third way involves an auxiliary radio clock driver which calls
 * the PPS driver with a timestamp captured by that driver. This use is
 * documented in the source code for the driver(s) involved.  Note that
 * some drivers collect the sample information themselves before calling
 * our pps_sample(), and others call us knowing only that they are running
 * shortly after an on-time tick and they expect us to retrieve the PPS
 * offset, fudge their result, and insert it into the timestream.
 *
 * Fudge Factors
 *
 * There are no special fudge factors other than the generic and those
 * explicitly defined above. The fudge time1 parameter can be used to
 * compensate for miscellaneous UART and OS delays. Allow about 247 us
 * for uart delays at 38400 bps and about 1 ms for SunOS streams
 * nonsense.
 */

/*
 * Interface definitions
 */
#ifndef PPSX
#define	DEVICE		"/dev/pps%d" /* device name and unit */
#ifdef B38400
#define	SPEED232	B38400	/* uart speed (38400 baud) */
#else
#define SPEED232	EXTB	/* as above */
#endif
#endif /* PPSX */
#define	PRECISION	(-20)	/* precision assumed (about 1 usec) */
#define	REFID		"PPS\0"	/* reference ID */
#define	DESCRIPTION	"PPS Clock Discipline" /* WRU */

#define PPSMAXDISPERSE	(FP_SECOND / 100) /* max sample dispersion */
#define	NSAMPLES	32	/* final stages of median filter */
#ifdef PPS
#define PPS_POLL	2	/* ppsclock poll interval (s) */
#endif /* PPS */

/*
 * Imported from ntp_timer module
 */
extern u_long current_time;	/* current time (s) */
extern struct event timerqueue[]; /* inner space */

/*
 * Imported from ntpd module
 */
extern int debug;		/* global debug flag */

/*
 * Imported from ntp_loopfilter module
 */
#ifndef PPSX
extern int fdpps;		/* pps file descriptor */
#endif /* PPSX */
extern int pps_update;		/* prefer peer valid update */

/*
 * Imported from ntp_proto module
 */
extern struct peer *sys_peer;	/* somebody in charge */

/*
 * Unit control structure
 */
struct atomunit {
#ifdef PPS
	struct	event timer;	/* pps poll interval timer */
	struct	ppsclockev ev;	/* ppsclock control */
#endif /* PPS */
	int	pollcnt;	/* poll message counter */
};

/*
 * Global variables
 */
static struct peer *last_atom_peer;	/* peer structure pointer */

/*
 * Function prototypes
 */
static	int	atom_start	P((int, struct peer *));
static	void	atom_shutdown	P((int, struct peer *));
static	void	atom_poll	P((int, struct peer *));
#ifdef PPS
static	void	atom_pps	P((struct peer *));
#endif /* PPS */

/*
 * Transfer vector
 */
struct	refclock refclock_atom = {
	atom_start,		/* start up driver */
	atom_shutdown,		/* shut down driver */
	atom_poll,		/* transmit poll message */
	noentry,		/* not used (old atom_control) */
	noentry,		/* initialize driver */
	noentry,		/* not used (old atom_buginfo) */
	NOFLAGS			/* not used */
};


/*
 * atom_start - initialize data for processing
 */
static int
atom_start(unit, peer)
	int unit;
	struct peer *peer;
{
	register struct atomunit *up;
	struct refclockproc *pp;

#ifdef	DEBUG
	if (debug > 0)
		printf("atom_start: unit %d\n", unit);
#endif
	/*
	 * Allocate and initialize unit structure
	 */
	if (!(up = (struct atomunit *)
	    emalloc(sizeof(struct atomunit))))
		return (0);
	memset((char *)up, 0, sizeof(struct atomunit));
	pp = peer->procptr;
	pp->unitptr = (caddr_t)up;

	/*
	 * Initialize miscellaneous variables
	 */
	peer->precision = PRECISION;
	pp->clockdesc = DESCRIPTION;
	memcpy((char *)&pp->refid, REFID, 4);
	up->pollcnt = 2;
	pp->nstages = MAXSTAGE;

#ifdef PPS
	/*
	 * Arm the timer for the first interrupt. Give it ten seconds to
	 * allow the ppsclock line to be configured, since it could be
	 * assigned to another driver.
	 */
	up->timer.peer = (struct peer *)peer;
	up->timer.event_handler = atom_pps;
	up->timer.event_time = current_time + 10;
	TIMER_INSERT(timerqueue, &up->timer);
#endif /* PPS */
	last_atom_peer = peer;
	return (1);
}


/*
 * atom_shutdown - shut down the clock
 */
static void
atom_shutdown(unit, peer)
	int unit;
	struct peer *peer;
{
	register struct atomunit *up;
	struct refclockproc *pp;

#ifdef	DEBUG
	if (debug > 0)
		printf("atom_shutdown: unit %d\n", unit);
#endif
	pp = peer->procptr;
	up = (struct atomunit *)pp->unitptr;

	if (last_atom_peer == peer)
		last_atom_peer = 0;
#ifdef PPS
	TIMER_DEQUEUE(&up->timer);
#endif /* PPS */
	if (pp->io.fd)
		io_closeclock(&pp->io);
	free(up);
}

/*
 * pps_sample - process pps sample offset -- backwards compatible interface
 */
int
pps_sample(tsr)
	l_fp *tsr;
{
	struct peer *peer;
	struct refclockproc *pp;
	register struct atomunit *up;
	int i;
	l_fp lftemp;		/* l_fp temps */

#ifdef	DEBUG
	if (debug > 2)
		printf("pps_sample: pollcnt %d\n", up->pollcnt);
#endif
	/*
	 * This routine is called once per second by an auxilliary
	 * routine in another driver. It saves the sign-extended
	 * fraction supplied in the argument in a circular buffer for
	 * processing at the next poll event.
	 */
	peer = last_atom_peer;
	if (!peer)
		return (-1);	/* no ATOM configured ? Forget it ! */

	pp = peer->procptr;
	up = (struct atomunit *)pp->unitptr;

	L_CLR(&lftemp);
	L_ADDF(&lftemp, tsr->l_f);
	i = ((int)(pp->coderecv)) % pp->nstages;
	pp->filter[i] = lftemp;
	if (pp->coderecv == 0)
		for (i = 1; i < pp->nstages; i++)
			pp->filter[i] = pp->filter[0];
	pp->coderecv++;
	up->pollcnt = 2;

	/* HACK -- use the local UN*X clock to get the time -- this is wrong */
	pp->lastrec.l_ui = time(0) - 2 + JAN_1970;
	pp->lastrec.l_uf = 0;

	return (0);
}

#ifdef PPS
/*
 * atom_pps - receive data from the LDISC_PPS discipline
 */
static void
atom_pps(peer)
	struct peer *peer;
{
	register struct atomunit *up;
	struct refclockproc *pp;
	l_fp lftmp;
	int i;

	/*
	 * This routine is called once per second when the LDISC_PPS
	 * discipline is present. It snatches the pps timestamp from the
	 * kernel and saves the sign-extended fraction in a circular
	 * buffer for processing at the next poll event.
	 */
	pp = peer->procptr;
	up = (struct atomunit *)pp->unitptr;

#ifdef	DEBUG
	if (debug > 3)
		printf("atom_pps: pollcnt %d, fdpps = %d, serial = %ld\n",
		       up->pollcnt, fdpps, up->ev.serial);
#endif
	/*
	 * Arm the timer for the next interrupt
	 */
	up->timer.event_time = current_time + PPS_POLL;
	TIMER_INSERT(timerqueue, &up->timer);

	/*
	 * Convert the timeval to l_fp and save for billboards. Sign-
	 * extend the fraction and stash in the buffer. No harm is done
	 * if previous data are overwritten. If the discipline comes bum
	 * or the data grow stale, just forget it.
	 */ 
	i = up->ev.serial;
	if (ioctl(fdpps, CIOGETEV, (caddr_t)&up->ev) < 0)
		return;
	if (i == up->ev.serial)
		return;
	pp->lastrec.l_ui = up->ev.tv.tv_sec + JAN_1970;
	TVUTOTSF(up->ev.tv.tv_usec, pp->lastrec.l_uf);
	L_CLR(&lftmp);
	L_ADDF(&lftmp, pp->lastrec.l_f);
	L_NEG(&lftmp);
	i = ((int)(pp->coderecv)) % pp->nstages;
	pp->filter[i] = lftmp;
	if (pp->coderecv == 0)
		for (i = 1; i < pp->nstages; i++)
			pp->filter[i] = pp->filter[0];
	pp->coderecv++;
	up->pollcnt = 2;
}
#endif /* PPS */

#ifndef PPSX
static	void	atom_receive	P((struct recvbuf *));
/*
 * atom_receive - receive data from the serial line interface
 */
static void
atom_receive(rbufp)
	struct recvbuf *rbufp;
{
	register struct atomunit *up;
	struct refclockproc *pp;
	struct peer *peer;
	l_fp lftmp;
	int i;

	/*
	 * This routine is called once per second when the serial
	 * interface is in use. It snatches the timestamp from the
	 * buffer and saves the sign-extended fraction in a circular
	 * buffer for processing at the next poll event.
	 */
	peer = (struct peer *)rbufp->recv_srcclock;
	pp = peer->procptr;
	up = (struct atomunit *)pp->unitptr;
	pp->lencode = refclock_gtlin(rbufp, pp->a_lastcode, BMAX,
	    &pp->lastrec);
#ifdef	DEBUG
	if (debug > 2)
		printf("atom_receive: pollcnt %d, lastrec %ld\n",
		       up->pollcnt, pp->lastrec);
#endif

	/*
	 * Save the timestamp for billboards. Sign-extend the fraction
	 * and stash in the buffer. No harm is done if previous data are
	 * overwritten.
	 */
	L_CLR(&lftmp);
	L_ADDF(&lftmp, pp->lastrec.l_f);
	L_NEG(&lftmp);
	i = ((int)(pp->coderecv)) % pp->nstages;
	pp->filter[i] = lftmp;
	if (pp->coderecv == 0)
		for (i = 1; i < pp->nstages; i++)
			pp->filter[i] = pp->filter[0];
	pp->coderecv++;
	up->pollcnt = 2;
}
#endif /* PPSX */

/*
 * Compare two l_fp's - used with qsort()
 */
static int
atom_cmpl_fp(p1, p2)
	register const void *p1, *p2;	/* l_fp to compare */
{

	if (!L_ISGEQ((l_fp *)p1, (l_fp *)p2))
		return (-1);
	if (L_ISEQU((l_fp *)p1, (l_fp *)p2))
		return (0);
	return (1);
}

/*
 * atom_poll - called by the transmit procedure
 */
static void
atom_poll(unit, peer)
	int unit;
	struct peer *peer;
{
	register struct atomunit *up;
	struct refclockproc *pp;
	int i, n;
	l_fp median, lftmp;
	l_fp off[MAXSTAGE];
	u_fp disp;

#ifdef	DEBUG
	if (debug > 2)
		printf("atom_poll: unit %d\n", unit);
#endif
	/*
	 * At each poll we check for timeout. At the first timeout we
	 * test to see if the LDISC_PPS discipline is present and, if
	 * so, use that. If not, we attempt to open a serial line with
	 * LDISC_CLKPPS discipline. If that fails, we bitch to the log
	 * and clam up.
	 */
	pp = peer->procptr;
	up = (struct atomunit *)pp->unitptr;
	pp->polls++;
	if (up->pollcnt == 0) {
		refclock_report(peer, CEVNT_FAULT);
		return;
	}
	up->pollcnt--;

#ifndef PPSX
	if (up->pollcnt == 0) {
		if (!pp->io.fd && fdpps == -1) {
			int fd;
			char device[20];

			/*
			 * Open serial port. Use CLKPPS line discipline,
			 * if available. If unavailable, the code works
			 * anyway, but at reduced accuracy.
			 */
			(void)sprintf(device, DEVICE, unit);
			if (!(fd = refclock_open(device, SPEED232,
	 		    LDISC_CLKPPS))) {
				refclock_report(peer, CEVNT_FAULT);
				return;
			}
			fdpps = fd;	/* Linux hack!!! */
			pp->io.clock_recv = atom_receive;
			pp->io.srcclock = (caddr_t)peer;
			pp->io.datalen = 0;
			pp->io.fd = fd;
			if (!io_addclock(&pp->io)) {
				(void) close(fd);
				refclock_report(peer, CEVNT_FAULT);
				return;
			}
		}
	}

#endif /* PPSX */

 	/*
	 * Valid time (leap bits zero) is returned only if the prefer
	 * peer has survived the intersection algorithm and within
	 * CLOCK_MAX of local time and not too long ago.  This ensures
	 * the pps time is within +-0.5 s of the local time and the
	 * seconds numbering is unambiguous.
	 */
	if (pps_update) {
		pp->leap = 0;
		pp->lasttime = current_time;
	} else
		pp->leap = LEAP_NOTINSYNC;

	/*
	 * Copy the raw offsets and sort into ascending order
	 */
	for (i = 0; i < MAXSTAGE; i++)
		off[i] = pp->filter[i];
	qsort((char *)off, pp->nstages, sizeof(l_fp), atom_cmpl_fp);

	/*
	 * Reject the furthest from the median of nstages samples until
	 * nskeep samples remain.
	 */
	i = 0;
	n = pp->nstages;
	while ((n - i) > NSAMPLES) {
		lftmp = off[n - 1];
		median = off[(n + i) / 2];
		L_SUB(&lftmp, &median);
		L_SUB(&median, &off[i]);
		if (L_ISHIS(&median, &lftmp)) {
			/* reject low end */
			i++;
		} else {
			/* reject high end */
			n--;
		}
	}

	/*
	 * Compute the dispersion based on the difference between the
	 * extremes of the remaining offsets. Add to this the time since
	 * the last clock update, which represents the dispersion
	 * increase with time. We know that NTP_MAXSKEW is 16. If the
	 * sum is greater than the allowed sample dispersion, bail out.
	 * Otherwise, return the median offset plus the configured
	 * fudgetime1 value.
	 */
	lftmp = off[n - 1];
	L_SUB(&lftmp, &off[i]);
	disp = LFPTOFP(&lftmp) + current_time - pp->lasttime;
	if (disp > PPSMAXDISPERSE) {
		refclock_report(peer, CEVNT_BADTIME);
		pp->leap = LEAP_NOTINSYNC;
	}
	pp->offset = off[(n + 1) / 2];
	L_ADD(&pp->offset, &pp->fudgetime1);
	pp->dispersion = disp;

	if (pp->leap == 0)
	  refclock_report(peer, CEVNT_NOMINAL);
	
	refclock_receive(peer, &pp->offset, 0, pp->dispersion,
			 &pp->lastrec, &pp->lastrec, pp->leap);
}

#endif /* ATOM && REFCLOCK */
