[alt.sources] Set time from WWV clock

turner@ksr.com (James M. Turner) (11/06/90)

[  NOTE:  RICH SALZ DID NOT WRITE THIS; HE IS JUST POSTING IT!!! ]

After I finally got sick of hand-setting my master-machine to the
talking clock, and decided I wanted a little more accuracy and
automation, I wrote the following.  It works under SunOS 4.0.3, no
idea how it will do on other machines.  If you get it working on a new
architecture, send the fixes to me and I'll incorporate them in the next
release.

Place the file below in utcclock.c.  Instructions for compilation and
use are in the file itself.
------------------------------ Cut Here ------------------------------

/* UTCCLOCK.C - Set time from US naval Observatory via modem
   Copyright (C) 1990, James M. Turner

                               ***

   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 1, 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.

                               ***

   utcclock allows us mortals without cessium clocks and WWV radio
   receivers to keep our time reasonable close to UTC.

   Experiments on a Sun 3/60 had local time within 50 milliseconds of
   UTC as provided by the US Naval Observatory.  Not perfect, but better than
   trying to set it by hand.

   Usage: utcclock [-S] [-s speed] [-l device] [-n number]

   Where speed is the baud rate to use, device is the device to call on,
   and number is the phone number to dial.  If the -S option is used, the
   time will be set, otherwise only the delta difference is reported.  You
   must be root to set the time.  Because it uses the UUCP locking mechanism,
   it needs write permission to /usr/spool/uucp.  I installed it setuid with
   owner = uucp on my system.  If you install it setuid to root, anyone will
   be able to set the time.

   On SunOS 4.0.3, compile with:

   cc -O -o utcclock utcclock.c

   I suggest installing it in /usr/local/etc

   Have fun, and happy ticking.

   James Turner
   (turner@ksr.com)
   617-895-9480

   $Log:	utcclock.c,v $
 * Revision 1.4  90/11/05  16:27:56  turner
 * Added GPL notice
 * 
 * Revision 1.3  90/11/05  16:06:06  turner
 * Added exclusive locking of device, better leap-year computation.
 * 
   */

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

char version[] = "$Id: utcclock.c,v 1.4 90/11/05 16:27:56 turner Exp $";

/* PLEASE CHECK AND SET THE VALUES BELOW THIS LINE !!! */

/* Default device to use.  Should be exclusive-locking if possible */
#define TTYPORT "cua0"

/* Phone number to dial.  Please verify that you receive tone on this number
   (we wouldnt want to harrass little old ladies with modem tones, now would
   we?)
   */
#define NUMBER "12026530351"	/* US Naval Observatory */

/* Format of string to send to modem with number in %s */
#define DIALSTRING "ATDT%s\r"

/* Default baud rate to use on line */
#define BAUDRATE B1200

/* Number of seconds of data to read */
#define NCOUNT 5

/* Locking file for UUCP. %s will be replaced by "ntp" and the device name */
#define LOCKFILE "/usr/spool/uucp/LCK..%s"

/* END OF USER CONFIGURABLE SECTION */

#define FIRST_YEAR 1988
#define FIRST_VALUE 47160	/* The day value of Dec 31, FIRST_YEAR-1 */

int months[] = {
  31, 28,31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

int modem;			/* Device Descriptor for Modem Line */
char *timeout_reason=NULL;	/* If we timeout, where are we in the code? */

char lck1[80], lck2[80];

/* This code handles the cleanup (shutting down the modem) if you
abort for some reason */

void
exit_handler(sig, code, scp, addr)
int sig, code;
struct sigcontext *scp;
char *addr;
{
  int flags =  (TIOCM_DTR | TIOCM_LE);
  ioctl(modem, TIOCMBIC, &flags);
  if (sig == SIGALRM)
    printf("utcclock: timeout while %s\n", timeout_reason);
  if (timeout_reason != NULL) {
    unlink(lck1);
    unlink(lck2);
  }
  exit(0);
}

struct sigvec vec = {
  exit_handler,
  0,
  0};

/* Returns number of days in the given year.
 * Remember, a leap year is a year which is evenly divisible by 4,
 * but not a century, unless it is a century divisible by 400
 */
static
daysinyear(thisyear)
    int thisyear;
{
    if ((thisyear % 4) == 0 && (((thisyear % 100) != 0) ||
           ((thisyear % 400) == 0)))
       return(366);
    return(365);
}

main(argc, argv)
int argc;
char **argv;
{
  extern char *optarg;
  extern int optind;
  int fd1, flags;
  struct sigvec ovec;
  struct termio tty;
  char c, *OptLine, *OptNumber, option, device[50];
  struct tm tm;
  struct timeval del, del1;
  struct timezone tzp;
  struct timeb tp;
  time_t i;
  long utc_time;
  int hh, mm, ss, dd, yy, dummy, milli[3], m, n = 0, r, k=0, des,
      yydd, year, month, OptSetme = 0, OptSpeed = BAUDRATE;
  char buffer[80];
  

  /* Make sure that timeouts and terminations clean up after themselves */
  if (sigvec(SIGTERM, &vec, &ovec) != 0) {
    perror("Sigvec:");
    exit(-1);
  }
  if (sigvec(SIGINT, &vec, &ovec) != 0) {
    perror("Sigvec:");
    exit(-1);
  }
  if (sigvec(SIGALRM, &vec, &ovec) != 0) {
    perror("Sigvec:");
    exit(-1);
  }

  OptLine = TTYPORT;
  OptNumber = NUMBER;

  /* Read in the options */
  while ((option = getopt(argc, argv, "Sl:s:n:")) != -1) {
    switch (option) {
    case 'S':
      OptSetme = 1;
      break;
    case 'l':
      OptLine = optarg;
      break;
    case 'n':
      OptNumber = optarg;
      break;
    case 's':
      OptSpeed = atoi(optarg);
      switch (OptSpeed) {
      case 300:
	OptSpeed = B300;
	break;
      case 1200:
	OptSpeed = B1200;
	break;
      case 2400:
	OptSpeed = B2400;
	break;
      case 4800:
	OptSpeed = B4800;
	break;
      case 9600:
	OptSpeed = B9600;
	break;
      case 19200:
	OptSpeed = B19200;
	break;
      otherwise:
	fprintf(stderr, "utcclock: Illegal baud rate\n");
	exit(2);
      }
      break;
    case '?':
      fprintf(stderr, "Usage: utcclock [-S] [-s speed] [-l line] [n number]\n");
      exit(2);
      break;
    }
  }

  sprintf(device, "/dev/%s", OptLine);
  /* 60 Seconds to get the mess worked out */
  alarm(60);
  sprintf(lck1, LOCKFILE, "ntp");
  sprintf(lck2, LOCKFILE, OptLine);

  if ((n = open(lck1, O_CREAT|O_EXCL)) == -1) {
    fprintf(stderr, "utcclock: utcclock already running\n");
    exit(2);
  }
  close(n);
  if ((m = open(lck2, O_CREAT|O_EXCL)) == -1) {
    fprintf(stderr, "utcclock: port in use\n");
    unlink(lck1);
    exit(2);
  }
  close(m);

  timeout_reason = "opening device";
  if ((modem = open(device, O_RDWR|O_NDELAY|O_EXCL)) == -1) {
    perror("utcclock");
    exit(2);
  }
  timeout_reason = "setting line parameters";
  if (ioctl(des, TCGETA, &tty) == 0) {
    tty.c_cflag &= ~CBAUD;
    tty.c_cflag |= (CLOCAL | OptSpeed);
  }
  ioctl(des, TCSETA, &tty);
  fcntl(modem,F_SETFL,0);	/* Turn off NDELAY. */
  /* Mom says we have to reopen for effect */
  fd1 = open(device, O_RDWR|O_EXCL);
  close(modem);			/* Close old line */
  modem = fd1;

  /* dial utc */
  timeout_reason = "dialing";
  ioctl(modem, TCFLSH, 0);	/* Flush any old characters */
  sprintf(buffer, DIALSTRING, OptNumber); /* Change for non-hayes */
  write(modem, buffer, strlen(buffer));

  /* Now we read characters until we see the first * from the remote
   side.  I hope your modem doesn't report messages with a *
   */
  timeout_reason = "Looking for *";
  for (n = 0, buffer[0] = ' '; (n > -1), (buffer[0] != '*');
       n = read(modem, buffer, 1));
  if (n == -1) exit(2);

  yy = 0;			/* Set year = 0 */
  n = 0;			/* Set character counter = 0 */
  while (k < NCOUNT) {		/* Until we get NCOUNT seconds of data */
    i = read(modem, buffer+n, 1); /* Read a character */
    if (i <= 0) exit(-1);	/* On EOF, punt */
    if (buffer[n] == '*') {	/* If second-marker */
      if (yy != 0) {		/* Have we got legal data? */
	if (k == (NCOUNT-1)) {	/* And have we seen NCOUNT seconds? */
	  ftime(&tp);		/* Get our local time for delta */
	  if (OptSetme) {	/* If we set time */
	    r = settimeofday(&del1, &tzp); /* Set to calculated value */
	    if (r < 0)		/* Did we get an error? */
	      perror("settimeofday");
	  }
	  if (del1.tv_sec > tp.time) { /* If value is negative */
	    if (tp.millitm != 0) { /* And some part of it is millitm */
	      tp.time++;	/* Then delta is one second less */
	      tp.millitm = 1000 - tp.millitm; /* Plus 1.0 - millitm */
	    }
	    printf("Our clock slow by %d.%03d seconds\n",
		   del1.tv_sec - tp.time, tp.millitm);
	  } else {
	    printf("Our clock fast by %d.%03d seconds\n", 
		   tp.time - del1.tv_sec, tp.millitm);
	  }
	  printf("Universal Coordinated Time exactly %d/%d/%d %d:%02d:%02d\n", 
		 month, dd, yy, hh, mm, ss);
	}
	/* If we aren't ready to set the time yet, add one to the timestamp
	   and one to the good marker count */
	del1.tv_sec++;		
	k++;
      }
    } else {
      /* If we got a CR, we figure out the time sent */
      if (buffer[n] == '\n' || buffer[n] == 'r') {
	buffer[n] = 0;		/* Terminate buffer */
	/* If we haven't read a good value, and there are characters in the
	   buffer, and the string parses correctly, then calculate time
	   */
	if ((yy == 0) && 
	    (n != 0) &&
	    (sscanf(buffer, "%d %d %2d%2d%2d UTC", &yydd, &dd, &hh, &mm, &ss) 
	     == 5)) {

	  yydd -= FIRST_VALUE;	/* Subtract day value of Dec 31st */
	  for (year = FIRST_YEAR; (yydd > daysinyear(year)); year++)
	       /* Decrement until under 1 year left */
	       yydd -= daysinyear(year);
	  yy = year - 1900; /* Set year number */
	  if (daysinyear(year) == 366)
	       months[1] = 29; /* Handle leap year */
	  for (month = 0; (dd > months[month]); month++)
	    dd -= months[month];

	  tm.tm_sec = ss;	/* Stuff seconds in second field */
	  tm.tm_min = mm;	/* Stuff minutes in minute field */
	  tm.tm_hour = hh;	/* Stuff hours in hour field */
	  tm.tm_year = yy;	/* Stuff year in year field */
	  tm.tm_mday = dd;	/* Stuff day of month */
	  tm.tm_mon = month;	/* Stuff month */
	  utc_time = ((long)timegm(&tm)); /* Get gmtime version of time */
	  gettimeofday(&del1, &tzp); /* Get time zone information */
	  del1.tv_usec = 0;	/* Always exactly on second */
	  del1.tv_sec = utc_time; /* * is for *NEXT* second */
	}
	n = 0;			/* Reset character counter */
      } else {
	n++;			/* Store character */
	buffer[n] = 0;		/* Terminate */
      }
    }
  }
  flags = TIOCM_DTR;		/* Clean up after success */
  ioctl(modem, TIOCMBIC, &flags);
  unlink(lck1);
  unlink(lck2);
  exit(0);
}
-- 
Name:    James M. Turner            * Great Moments in Aviation #21: While on a
Company: Kendall Square Research    * NDB approach into Hanscom, pilot Ted Hertz
Email:   turner@ksr.com, ksr!turner * accidently tunes WEEI on his NAV instead
Phone:   (617) 895-9400             * of the LOM, and lands on top of the Pru.