/*
 * refclock_usno - clock driver for the Naval Observatory dialup
 * Michael Shields <shields@tembel.org> 1995/02/25
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#if defined(REFCLOCK) && defined(CLOCK_USNO)

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

#include <stdio.h>
#include <ctype.h>
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>
#endif /* HAVE_SYS_IOCTL_H */

/*
 * This driver supports the Naval Observatory dialup at +1 202 653 0351.
 * It is a hacked-up version of the ACTS driver.
 *
 * This driver does not support the `phone' configuration because that
 * is needlessly global; it would clash with the ACTS driver.
 *
 * The Naval Observatory does not support the echo-delay measurement scheme.
 *
 * However, this driver *does* support UUCP port locking, allowing the
 * line to be shared with other processes when not actually dialing
 * for time.
 */

/*
 * Interface definitions
 */

#define	DEVICE		"/dev/cua%d" /* device name and unit */
#define LOCKFILE	"/var/lock/LCK..cua%d"
/* #define LOCKFILE	"/usr/spool/uucp/LCK..cua%d" */

#define PHONE		"atdt 202 653 0351"
/* #define PHONE	"atdt 1 202 653 0351" */

#define	SPEED232	B1200	/* uart speed (1200 cowardly baud) */
#define	PRECISION	(-10)	/* precision assumed (about 1 ms) */
#define	REFID		"USNO"	/* reference ID */
#define	DESCRIPTION	"Naval Observatory dialup"

#define MODE_AUTO	0	/* automatic mode */
#define MODE_BACKUP	1	/* backup mode */
#define MODE_MANUAL	2	/* manual mode */

#define MSGCNT		10	/* we need this many time messages */
#define SMAX		80	/* max token string length */
#define LENCODE		20	/* length of valid timecode string */
#define USNO_MINPOLL	10	/* log2 min poll interval (1024 s) */
#define USNO_MAXPOLL	14	/* log2 max poll interval (16384 s) */
#define MAXOUTAGE	3600	/* max before USNO kicks in (s) */

/*
 * Modem control strings. These may have to be changed for some modems.
 *
 * AT	command prefix
 * B1	initiate call negotiation using Bell 212A
 * &C1	enable carrier detect
 * &D2	hang up and return to command mode on DTR transition
 * E0	modem command echo disabled
 * l1	set modem speaker volume to low level
 * M1	speaker enabled untill carrier detect
 * Q0	return result codes
 * V1	return result codes as English words
 */
#define MODEM_SETUP	"ATB1&C1&D2E0L1M1Q0V1" /* modem setup */
#define MODEM_HANGUP	"ATH"	/* modem disconnect */

/*
 * Timeouts
 */
#define IDLE		60	/* idle timeout (s) */
#define WAIT		2	/* wait timeout (s) */
#define ANSWER		30	/* answer timeout (s) */
#define CONNECT		10	/* connect timeout (s) */
#define TIMECODE	(MSGCNT+16)	/* timecode timeout (s) */

/*
 * Unit control structure
 */
struct usnounit {
	int	pollcnt;	/* poll message counter */

	int	state;		/* the first one was Delaware */
	int	run;		/* call program run switch */
	int	msgcnt;		/* count of time messages received */
	long	redial;		/* interval to next automatic call */
	int	unit;		/* unit number (= port) */
};

/*
 * Function prototypes
 */
static	int	usno_start	P((int, struct peer *));
static	void	usno_shutdown	P((int, struct peer *));
static	void	usno_poll	P((int, struct peer *));
static	void	usno_disc	P((struct peer *));
#if 0
static	void	usno_timeout	P((struct peer *));
static	void	usno_receive	P((struct recvbuf *));
static	int	usno_write	P((struct peer *, const char *));
#endif /* 0 */

/*
 * Transfer vector
 */
struct	refclock refclock_usno = {
	usno_start,		/* start up driver */
	usno_shutdown,		/* shut down driver */
	usno_poll,		/* transmit poll message */
	noentry,		/* not used (usno_control) */
	noentry,		/* not used (usno_init) */
	noentry,		/* not used (usno_buginfo) */
	NOFLAGS			/* not used */
};


/*
 * usno_start - open the devices and initialize data for processing
 */
static int
usno_start(
	int unit,
	struct peer *peer
	)
{
	register struct usnounit *up;
	struct refclockproc *pp;

	/*
	 * Initialize miscellaneous variables
	 */
	pp = peer->procptr;
	peer->precision = PRECISION;
	pp->clockdesc = DESCRIPTION;
	memcpy((char *)&pp->refid, REFID, 4);
	peer->minpoll = USNO_MINPOLL;
	peer->maxpoll = USNO_MAXPOLL;
	peer->sstclktype = CTL_SST_TS_TELEPHONE;

	/*
	 * Allocate and initialize unit structure
	 */
	if (!(up = (struct usnounit *)
	      emalloc(sizeof(struct usnounit))))
	    return (0);
	memset((char *)up, 0, sizeof(struct usnounit));
	up->unit = unit;
	pp->unitptr = (caddr_t)up;

	/*
	 * Set up the driver timeout
	 */
	peer->nextdate = current_time + WAIT;
	return (1);
}


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

#ifdef DEBUG
	if (debug)
	    printf("usno: clock %s shutting down\n", ntoa(&peer->srcadr));
#endif
	pp = peer->procptr;
	up = (struct usnounit *)pp->unitptr;
	usno_disc(peer);
	free(up);
}


#if 0
/*
 * usno_receive - receive data from the serial interface
 */
static void
usno_receive(
	struct recvbuf *rbufp
	)
{
	register struct usnounit *up;
	struct refclockproc *pp;
	struct peer *peer;
	char str[SMAX];
	u_long mjd;		/* Modified Julian Day */
	static int day, hour, minute, second;

	/*
	 * Initialize pointers and read the timecode and timestamp. If
	 * the OK modem status code, leave it where folks can find it.
	 */
	peer = (struct peer *)rbufp->recv_srcclock;
	pp = peer->procptr;
	up = (struct usnounit *)pp->unitptr;
	pp->lencode = refclock_gtlin(rbufp, pp->a_lastcode, BMAX,
				     &pp->lastrec);
	if (pp->lencode == 0) {
		if (strcmp(pp->a_lastcode, "OK") == 0)
		    pp->lencode = 2;
		return;
	}
#ifdef DEBUG
	if (debug)
	    printf("usno: timecode %d %s\n", pp->lencode,
		   pp->a_lastcode);
#endif

	switch (up->state) {

	    case 0:

		/*
		 * State 0. We are not expecting anything. Probably
		 * modem disconnect noise. Go back to sleep.
		 */
		return;

	    case 1:

		/*
		 * State 1. We are about to dial. Just drop it.
		 */
		return;

	    case 2:

		/*
		 * State 2. We are waiting for the call to be answered.
		 * All we care about here is CONNECT as the first token
		 * in the string. If the modem signals BUSY, ERROR, NO
		 * ANSWER, NO CARRIER or NO DIALTONE, we immediately
		 * hang up the phone. If CONNECT doesn't happen after
		 * ANSWER seconds, hang up the phone. If everything is
		 * okay, start the connect timeout and slide into state
		 * 3.
		 */
		(void)strncpy(str, strtok(pp->a_lastcode, " "), SMAX);
		if (strcmp(str, "BUSY") == 0 || strcmp(str, "ERROR") ==
		    0 || strcmp(str, "NO") == 0) {
			NLOG(NLOG_CLOCKINFO) /* conditional if clause for conditional syslog */
				msyslog(LOG_NOTICE,
					"clock %s USNO modem status %s",
					ntoa(&peer->srcadr), pp->a_lastcode);
			usno_disc(peer);
		} else if (strcmp(str, "CONNECT") == 0) {
			peer->nextdate = current_time + CONNECT;
			up->msgcnt = 0;
			up->state++;
		} else {
			NLOG(NLOG_CLOCKINFO) /* conditional if clause for conditional syslog */
				msyslog(LOG_WARNING,
					"clock %s USNO unknown modem status %s",
					ntoa(&peer->srcadr), pp->a_lastcode);
		}
		return;

	    case 3:

		/*
		 * State 3. The call has been answered and we are
		 * waiting for the first message. If this doesn't
		 * happen within the timecode timeout, hang up the
		 * phone. We probably got a wrong number or they are
		 * down.
		 */
		peer->nextdate = current_time + TIMECODE;
		up->state++;
		return;

	    case 4:

		/*
		 * State 4. We are reading a timecode.  It's an actual
		 * timecode, or it's the `*' OTM.
		 *
		 * jjjjj nnn hhmmss UTC
		 */
		if (pp->lencode == LENCODE) {
			if (sscanf(pp->a_lastcode, "%5ld %3d %2d%2d%2d UTC",
				   &mjd, &day, &hour, &minute, &second) != 5) {
#ifdef DEBUG
				if (debug)
				    printf("usno: bad timecode format\n");
#endif
				refclock_report(peer, CEVNT_BADREPLY);
			} else
			    up->msgcnt++;
			return;
		} else if (pp->lencode != 1 || !up->msgcnt)
		    return;
		/* else, OTM; drop out of switch */
	}

	pp->leap = LEAP_NOWARNING;
	pp->day = day;
	pp->hour = hour;
	pp->minute = minute;
	pp->second = second;

	/*
	 * Colossal hack here. We process each sample in a trimmed-mean
	 * filter and determine the reference clock offset and
	 * dispersion. The fudge time1 value is added to each sample as
	 * received.
	 */
	if (!refclock_process(pp)) {
#ifdef DEBUG
		if (debug)
		    printf("usno: time rejected\n");
#endif
		refclock_report(peer, CEVNT_BADTIME);
		return;
	} else if (up->msgcnt < MSGCNT)
	    return;

	/*
	 * We have a filtered sample offset ready for peer processing.
	 * We use lastrec as both the reference time and receive time in
	 * order to avoid being cute, like setting the reference time
	 * later than the receive time, which may cause a paranoid
	 * protocol module to chuck out the data. Finaly, we unhook the
	 * timeout, arm for the next call, fold the tent and go home.
	 */
	pp->lastref = pp->lastrec;
	refclock_receive(peer);
	record_clock_stats(&peer->srcadr, pp->a_lastcode);
	pp->sloppyclockflag &= ~CLK_FLAG1;
	up->pollcnt = 0;
	up->state = 0;
	usno_disc(peer);
}
#endif /* 0 */


/*
 * usno_poll - called by the transmit routine
 */
static void
usno_poll(
	int unit,
	struct peer *peer
	)
{
	register struct usnounit *up;
	struct refclockproc *pp;

	/*
	 * If the driver is running, we set the enable flag (fudge
	 * flag1), which causes the driver timeout routine to initiate a
	 * call. If not, the enable flag can be set using
	 * ntpdc. If this is the sustem peer, then follow the system
	 * poll interval.
	 */
	pp = peer->procptr;
	up = (struct usnounit *)pp->unitptr;
	if (up->run) {
		pp->sloppyclockflag |= CLK_FLAG1;
		if (peer == sys_peer)
		    peer->hpoll = sys_poll;
		else
		    peer->hpoll = peer->minpoll;
	}
}


#if 0
/*
 * usno_timeout - called by the timer interrupt
 */
static void
usno_timeout(
	struct peer *peer
	)
{
	register struct usnounit *up;
	struct refclockproc *pp;
	int fd;
	char device[20];
	char lockfile[128], pidbuf[8];
	int dtr = TIOCM_DTR;

	/*
	 * If a timeout occurs in other than state 0, the call has
	 * failed. If in state 0, we just see if there is other work to
	 * do.
	 */
	pp = peer->procptr;
	up = (struct usnounit *)pp->unitptr;
	if (up->state) {
		if (up->state != 1) {
			usno_disc(peer);
			return;
		}
		/*
		 * Call, and start the answer timeout. We think it
		 * strange if the OK status has not been received from
		 * the modem, but plow ahead anyway.
		 *
		 * This code is *here* because we had to stick in a brief
		 * delay to let the modem settle down after raising DTR,
		 * and for the OK to be received.  State machines are
		 * contorted.
		 */
		if (strcmp(pp->a_lastcode, "OK") != 0)
		    NLOG(NLOG_CLOCKINFO) /* conditional if clause for conditional syslog */
			    msyslog(LOG_NOTICE, "clock %s USNO no modem status",
				    ntoa(&peer->srcadr));
		(void)usno_write(peer, PHONE);
		NLOG(NLOG_CLOCKINFO) /* conditional if clause for conditional syslog */
			msyslog(LOG_NOTICE, "clock %s USNO calling %s\n",
				ntoa(&peer->srcadr), PHONE);
		up->state = 2;
		up->pollcnt++;
		pp->polls++;
		peer->nextdate = current_time + ANSWER;
		return;
	}
	switch (peer->ttl) {

		/*
		 * In manual mode the calling program is activated
		 * by the ntpdc program using the enable flag (fudge
		 * flag1), either manually or by a cron job.
		 */
	    case MODE_MANUAL:
		up->run = 0;
		break;

		/*
		 * In automatic mode the calling program runs
		 * continuously at intervals determined by the sys_poll
		 * variable.
		 */
	    case MODE_AUTO:
		if (!up->run)
		    pp->sloppyclockflag |= CLK_FLAG1;
		up->run = 1;
		break;

		/*
		 * In backup mode the calling program is disabled,
		 * unless no system peer has been selected for MAXOUTAGE
		 * (3600 s). Once enabled, it runs until some other NTP
		 * peer shows up.
		 */
	    case MODE_BACKUP:
		if (!up->run && sys_peer == 0) {
			if (current_time - last_time > MAXOUTAGE) {
				up->run = 1;
				peer->hpoll = peer->minpoll;
				NLOG(NLOG_CLOCKINFO) /* conditional if clause for conditional syslog */
					msyslog(LOG_NOTICE,
						"clock %s USNO backup started ",
						ntoa(&peer->srcadr));
			}
		} else if (up->run && sys_peer->sstclktype != CTL_SST_TS_TELEPHONE) {
			peer->hpoll = peer->minpoll;
			up->run = 0;
			NLOG(NLOG_CLOCKINFO) /* conditional if clause for conditional syslog */
				msyslog(LOG_NOTICE,
					"clock %s USNO backup stopped",
					ntoa(&peer->srcadr));
		}
		break;

	    default:
		msyslog(LOG_ERR,
			"clock %s USNO invalid mode", ntoa(&peer->srcadr));
		
	}

	/*
	 * The fudge flag1 is used as an enable/disable; if set either
	 * by the code or via ntpdc, the calling program is
	 * started; if reset, the phones stop ringing.
	 */
	if (!(pp->sloppyclockflag & CLK_FLAG1)) {
		up->pollcnt = 0;
		peer->nextdate = current_time + IDLE;
		return;
	}

	/*
	 * Lock the port.
	 */
	(void)sprintf(lockfile, LOCKFILE, up->unit);
	fd = open(lockfile, O_WRONLY|O_CREAT|O_EXCL, 0644);
	if (fd < 0) {
		msyslog(LOG_ERR, "clock %s USNO port busy",
			ntoa(&peer->srcadr));
		return;
	}
	sprintf(pidbuf, "%d\n", (int) getpid());
	write(fd, pidbuf, strlen(pidbuf));
	close(fd);

	/*
	 * Open serial port. Use ACTS line discipline, if available. It
	 * pumps a timestamp into the data stream at every on-time
	 * character '*' found. Note: the port must have modem control
	 * or deep pockets for the phone bill. HP-UX 9.03 users should
	 * have very deep pockets.
	 */
	(void)sprintf(device, DEVICE, up->unit);
	if (!(fd = refclock_open(device, SPEED232, LDISC_ACTS))) {
		unlink(lockfile);
		return;
	}
	if (ioctl(fd, TIOCMBIC, (char *)&dtr) < 0)
	    msyslog(LOG_WARNING, "usno_timeout: clock %s: couldn't clear DTR: %m",
		    ntoa(&peer->srcadr));

	pp->io.clock_recv = usno_receive;
	pp->io.srcclock = (caddr_t)peer;
	pp->io.datalen = 0;
	pp->io.fd = fd;
	if (!io_addclock(&pp->io)) {
		(void) close(fd);
		unlink(lockfile);
		free(up);
		return;
	}

	/*
	 * Initialize modem and kill DTR. We skedaddle if this comes
	 * bum.
	 */
	if (!usno_write(peer, MODEM_SETUP)) {
		msyslog(LOG_ERR, "clock %s USNO couldn't write",
			ntoa(&peer->srcadr));
		io_closeclock(&pp->io);
		unlink(lockfile);
		free(up);
		return;
	}

	/*
	 * Initiate a call to the Observatory. If we wind up here in
	 * other than state 0, a successful call could not be completed
	 * within minpoll seconds.
	 */
	if (up->pollcnt) {
		refclock_report(peer, CEVNT_TIMEOUT);
		NLOG(NLOG_CLOCKINFO) /* conditional if clause for conditional syslog */
			msyslog(LOG_NOTICE,
				"clock %s USNO calling program terminated",
				ntoa(&peer->srcadr));
		pp->sloppyclockflag &= ~CLK_FLAG1;
		up->pollcnt = 0;
#ifdef DEBUG
		if (debug)
		    printf("usno: calling program terminated\n");
#endif
		usno_disc(peer);
		return;
	}

	/*
	 * Raise DTR, and let the modem settle.  Then we'll dial.
	 */
	if (ioctl(pp->io.fd, TIOCMBIS, (char *)&dtr) < -1)
	    msyslog(LOG_INFO, "usno_timeout: clock %s: couldn't set DTR: %m",
		    ntoa(&peer->srcadr));
	up->state = 1;
	peer->nextdate = current_time + WAIT;
}
#endif /* 0 */


/*
 * usno_disc - disconnect the call and wait for the ruckus to cool
 */
static void
usno_disc(
	struct peer *peer
	)
{
	register struct usnounit *up;
	struct refclockproc *pp;
	int dtr = TIOCM_DTR;
	char lockfile[128];

	/*
	 * We should never get here other than in state 0, unless a call
	 * has timed out. We drop DTR, which will reliably get the modem
	 * off the air, even while the modem is hammering away full tilt.
	 */
	pp = peer->procptr;
	up = (struct usnounit *)pp->unitptr;

	if (ioctl(pp->io.fd, TIOCMBIC, (char *)&dtr) < 0)
	    msyslog(LOG_INFO, "usno_disc: clock %s: couldn't clear DTR: %m",
		    ntoa(&peer->srcadr));

	if (up->state > 0) {
		up->state = 0;
		msyslog(LOG_NOTICE, "clock %s USNO call failed %d",
			ntoa(&peer->srcadr), up->state);
#ifdef DEBUG
		if (debug)
		    printf("usno: call failed %d\n", up->state);
#endif
	}

	io_closeclock(&pp->io);
	sprintf(lockfile, LOCKFILE, up->unit);
	unlink(lockfile);

	peer->nextdate = current_time + WAIT;
}


#if 0
/*
 * usno_write - write a message to the serial port
 */
static int
usno_write(
	struct peer *peer,
	const char *str
	)
{
	register struct usnounit *up;
	struct refclockproc *pp;
	int len;
	int code;
	char cr = '\r';

	/*
	 * Not much to do here, other than send the message, handle
	 * debug and report faults.
	 */
	pp = peer->procptr;
	up = (struct usnounit *)pp->unitptr;
	len = strlen(str);
#ifdef DEBUG
	if (debug)
	    printf("usno: state %d send %d %s\n", up->state, len,
		   str);
#endif
	code = write(pp->io.fd, str, (unsigned)len) == len;
	code |= write(pp->io.fd, &cr, 1) == 1;
	if (!code)
	    refclock_report(peer, CEVNT_FAULT);
	return (code);
}
#endif /* 0 */

#else
int refclock_usno_bs;
#endif /* REFCLOCK */
