/*

    Set  UNIX  system  clock  from  the  atomic  clock at the National
    Institute of Standards and Technology (NIST) in Boulder  Colorado.

    Designed and implemented in May of 1991 by John Walker.

    This  program,  which should be run when in super-user mode, dials
    into the telephone time service maintained by the NIST in Boulder,
    obtains  the  time	from  the  atomic  clock  there (obtaining two
    consecutive valid readings to guard against communication errors),
    then sets the system clock at the moment the time hack is received
    for the next second.  While waiting for the tick, the program goes
    into  a high-priority CPU loop waiting for the tick to be received
    to obtain the best accuracy.

    The modem port, modem dial codes and telephone number, and various
    other  parameters  are  set on the command line.  The defaults are
    equivalent to the following:

    settime
	    -M/dev/cua0 	      -- Modem port
	    -NATDT13034944774	      -- Dial command and phone number
	    -T120		      -- Two minute timeout

    If you specify the "-E" option, all modem input and output will be
    echoed to standard output.

    You  don't  have  to be super-user to run the progrmam, but if you
    aren't the system time and date won't  be  changed,  as  only  the
    super-user may change them.

*/

#define REVDATE "13th May 1991"

#include <sys/types.h>
#include <sys/time.h>
#include <stdio.h>
#include <fcntl.h>
#include <termio.h>
#include <time.h>

#define     FALSE   0
#define     TRUE    1

#define     EOS     '\0'

struct termio newmode, oldmode;

#ifdef __linux
static char *modem = "/dev/ttys0";     /* Modem file name */
#else
static char *modem = "/dev/cua0";     /* Modem file name */
#endif
static char *dialstring = "ATDT13034944774"; /* Dial command */
static int echoin = FALSE;	      /* Echo modem info if nonzero */
static int port;		      /* Modem file descriptor */
static char line[132];		      /* Input buffer */
static int jdate1970 = 2440588;       /* Julian date of January 1, 1970 */
static long usdelay;		      /* Line delay in microseconds */
static time_t bailout;		      /* Time to give up if no success */

/*  TIMEDOUT  --  Check for timeout expired.  */

static int timedout()
{
    return time(NULL) > bailout;
}

/*  MREAD  --  Read next character from modem. */

static int mread(buf)
  char *buf;
{
    return read(port, buf, 1);
}


/*  INLINE  --	Obtain a newline terminated input line from the modem.	*/

static int inline()
{
    int l = 0;
    char buf;

    while (TRUE) {
	if (mread(&buf) > 0) {
	    if (echoin) {
		putchar(buf);
	    }
            if (buf == '\n') {
		line[l] = EOS;
		break;
	    }
            if (buf >= ' ' && l < 131) {
		line[l++] = buf;
	    }
	} else {
	    if (timedout()) {
                strcpy(line, "BUSY");
		return FALSE;
	    }
	    usleep(4000L);
	}
    }
    return TRUE;
}

/*  JDATE  --  Convert internal GMT date and time to Julian day
	       and fraction.  */

static long jdate(t)
  struct tm *t;
{
	long c, m, y;

	y = t->tm_year + 1900;
	m = t->tm_mon + 1;
	if (m > 2)
	   m = m - 3;
	else {
	   m = m + 9;
	   y--;
	}
	c = y / 100L;		   /* Compute century */
	y -= 100L * c;
	return t->tm_mday + (c * 146097L) / 4 + (y * 1461L) / 4 +
	    (m * 153L + 2) / 5 + 1721119L;
}

/*  PARSE_NIST  --  Parse NIST time string into a "tm" structure.  */

static int parse_nist()
{
    int jd, yr, mo, da, hh, mm, ss, dst, ls, msadvw, msadvf;
    char dut1[3], tname[20];
    time_t gt;
    struct tm t;

    if (sscanf(line, "%5d %2d-%2d-%2d %2d:%2d:%2d %2d %1d %3c %3d.%1d %s",
	&jd, &yr, &mo, &da, &hh, &mm, &ss, &dst, &ls, dut1, &msadvw,
	&msadvf, tname) < 13)
	return 0;

    if (strncmp(tname, "UTC(NIST)", 9) != 0)
	return 0;

    t.tm_sec = ss;
    t.tm_min = mm;
    t.tm_hour = hh;
    t.tm_mday = da;
    t.tm_mon = mo - 1;
    t.tm_year = (yr > 90) ? yr : (yr + 100);
    t.tm_isdst = (dst > 0) && (dst <= 50);
    t.tm_wday = ((jd + 3) % 7);

    usdelay = ((msadvw * 10) + msadvf) * 100;

    gt = ((((jdate(&t) - jdate1970) * 24 + t.tm_hour) * 60) +
	    t.tm_min) * 60 + t.tm_sec;

#ifdef DEBUG
{ char *cp = asctime(gmtime(&gt));
printf("Clock %d (%d) Date and time: %s Delay = %d\n", gt, time(NULL), cp,
    usdelay);
}
#endif
    return gt;
}

int main(argc, argv)
  int argc; char *argv[];
{
    int i, success;
    char *cp, opt;
    time_t ptime, ntime;
    struct timeval tv;
    int timeout = 120;		      /* How long to attempt to get time */

#ifdef TEST1
    strcpy(line, "48388 91-05-12 20:48:58 50 0 +.3 045.0 UTC(NIST) *");
    parse_nist(&ntime);
    return 0;
#endif

    for (i = 1; i < argc; i++) {
	cp = argv[i];
        if (*cp == '-') {
	    opt = *(++cp);
	    if (islower(opt))
		opt = toupper(opt);
	    switch (opt) {

                case 'E':
		    echoin = TRUE;
		    break;

                case 'M':
		    modem = cp + 1;
		    break;

                case 'N':
		    dialstring = cp + 1;
		    break;

                case 'T':
		    timeout = atoi(cp + 1);
		    break;

                case '?':
                case 'U':
    fprintf(stderr,"\nSETTIME  --  Set time from NIST time service.  Call");
    fprintf(stderr,
       "\n             with settime [options]");
    fprintf(stderr,"\n");
    fprintf(stderr,"\n         Options:");
    fprintf(stderr,"\n           -E       Echo modem input and output");
    fprintf(stderr,"\n           -Mdev    Use modem device dev");
    fprintf(stderr,"\n           -Ncmd    Use cmd as modem dial command");
    fprintf(stderr,"\n           -Tn      Set timeout to n seconds");
    fprintf(stderr,"\n");
    fprintf(stderr,"\n          (P) Throoput Ltd.  %s", REVDATE);
    fprintf(stderr,"\n                All Rights Reversed");
    fprintf(stderr,"\n");
		    return 0;
	    }
	} else {
	}
    }

    if ((port = open(modem, O_RDWR | O_NDELAY)) == -1) {
        fprintf(stderr, "Cannot open modem port %s\n", modem);
	return 2;
    }
    if (ioctl(port, TCGETA, &oldmode) == -1) {       /* Get current tty mode */
        perror(modem);
        return 2;
    }
    newmode = oldmode;		      /* Save it to restore port later */
    bailout = time(NULL) + timeout;   /* Set time to give up */

    if (echoin) {
	setbuf(stdout, NULL);	      /* Make echoed output unbuffered */
    }

    /* Set the modem port to the proper modes for calling the time
       service. */

    newmode.c_iflag = IGNBRK | IGNPAR | ISTRIP;
    newmode.c_oflag = 0;
    newmode.c_cflag = B1200 | CS7 | CREAD | PARENB | HUPCL;
    newmode.c_lflag = 0;
    if (ioctl(port, TCSETA, &newmode) == -1) {
        fprintf(stderr, "Cannot set modem to desired modes\n");
        perror(modem);
        return 2;
    }

    /* Dial the time service. */

#ifndef __linux
    if (fcntl(port, F_SETFL, FNDELAY) == -1) {
        fprintf(stderr, "Cannot set port to non-delayed response\n");
        perror(modem);
        return 2;
    }
#endif
    if (write(port, "\r\r\r", 3) != 3) {
        fprintf(stderr, "Error sending CR's to modem\n");
        perror(modem);
        return 2;
    }
    sleep(1);
    while (read(port, &opt, 1) > 0) ; /* Flush input */
    if (echoin) {
        printf("%s\n", dialstring);
    }
    if (write(port, dialstring, strlen(dialstring)) != strlen(dialstring) ||
        write(port, "\r", 1) != 1) {
        fprintf(stderr, "Error sending dial string to modem\n");
        perror(modem);
    }

    /* Wait until we're connected.  If we get a busy signal, give up
       and report an error status.  */

    while (inline()) {
        if ((strncmp(line, "CONNECT", 7) == 0) || (line[0] == '5')) {
	    break;
	}
        if (strncmp(line, "BUSY", 4) == 0 ||
            strncmp(line, "NO ", 3) == 0 ||
            strncmp(line, "ERROR", 5) == 0 ||
            line[0] == '3' || line[0] == '4' || line[0] == '6' ||
            line[0] == '7' || line[0] == '8') {
            fprintf(stderr, "Unable to connect.\n");
	    return 1;
	}
    }

    /*  At this point we're connected.  Ignore all data from the line
	until we receive two consecutive valid time signals one second
        apart.  Once we've received these two signals, prepare the
	time setting for the next second and spin until we receive
        the "*" that indicates the correct instant to set the clock.
	At that moment, change the system clock.  */

    ptime = 0;
    success = FALSE;
    while (!success && !timedout() && inline()) {
	if (ptime == 0) {
	    ptime = parse_nist();
	    ntime = 0;
	} else if (ntime == 0) {
	    ntime = parse_nist();
	    if (ntime == (ptime + 1)) {
		int rpyet = 0;

		tv.tv_sec = ntime + 1;
		tv.tv_usec = 0;
		while (TRUE) {
		    char buf;
		    int ticker = 0;   /* CPU loop timeout check */

		    if (mread(&buf) > 0) {
			if (echoin) {
			    putchar(buf);
			}
                        if ((buf == '*') || (buf == '#')) {
			    settimeofday(&tv, NULL);
			    success = TRUE;
			    nice(10);	   /* Drop to normal priority */
			    break;
                        } else if (buf == ')') {
			    rpyet = 1;
			    nice(-10);	   /* Set high priority */
                        } else if (buf < ' ') {
			    ptime = 0;
			    if (rpyet) {
				nice(10);  /* Drop to normal priority */
			    }
			}
		    } else {
			if (!rpyet) {
			    usleep(2000L);
			    if (timedout()) {
				break;
			    }
			} else {
			    if (((++ticker) % 100) == 0) {
				if (timedout()) {
				    break;
				}
			    }
			}
		    }
		}
	    } else {
		ptime = 0;
	    }
	}
    }

    if (echoin) {
        putchar('\n');
    }
    if (success) {
        printf("Universal time: %s", asctime(gmtime(&tv.tv_sec)));
        printf("Local time:     %s", asctime(localtime(&tv.tv_sec)));
    }

    /* Restore port to original modes and close, disconnecting the
       modem. */

    ioctl(port, TCSETAW, &oldmode);
    ioctl(port, TCFLSH, 2);
    close(port);

    return 0;
}
