Date: Thursday, 16 June 1988  16:55-MDT
From: attcan!utzoo!dciem!nrcaer!cognos!bruce at uunet.uu.net (Dwayne Bruce)
Re:   N.B.S. Time Service

Well kids, after reading about the NBS telephone time service, and
having read about an expensive commerical system from Precision
Standard Time, Inc., I just had to tell you about the system I cooked
up.

We've been keeping our VAXen in sync with a simple system that uses
the AFSK time code produced by CHU.  CHU is Canada's provider of
brodcast time, and it has a signal that can be heard regularly all
over the western hemisphere.  During seconds 31..39 of each minute,
they transmit a time code in 103-type AFSK.  I built a modem, plugged
it into an HF receiver, and wrote a piece of code to handle the
output. The code is enclosed.

If you want to know more about the hardware, consult the article I
wrote in the April, 1988 edition of "The Canadian Amateur" (published
by the Canadian Amateur Radio Federation).

Anyway, here's the code.  It runs under UNIX and VMS.

-------------------------
/* CHU      Marcus Leech  VE3MDL   Aug 1987 */
/* This program reads and understands the timecode format
*  produced by the CHU broadcast signal.  This signal provides
*  an atomic time standard to most places in North and South
*  America on 3.330 7.335 and 14.670 MHZ, mode A3. During
*  seconds 31 thru 39 the time is broadcast using Bell 103 AFSK.
*  The time code looks like this:
*  6.d|d.d|h.h|m.m|s.s|6.d|d.d|h.h|m.m|s.s
*  The time code is repeated twice for error detection. Each . represents
*    one nibble boundary.  The nibbles get transmitted reversed, so you
*    have to flip them to make sense of the time code.  Each nibble is a BCD
*    digit.
* It takes one argument, the input device, and some optional flags:
*  chu [-adst] <device>
*  -
*  a adjust  attempt to adjust the system clock to CHU time
*  d daemon  run continuously as a daemon
*  s show    show semi-raw timecode
*  t tune    show chars that wouldn't get considered for time code
*
* When used as a system-time-adjuster, you need to set the system time
*  to within 2mins of the correct time. The program will drag the
*  clock into exact synchronism.
*/
#include <stdio.h>
#ifndef VMS
#include <sys/types.h>
#include <sgtty.h>
#include <time.h>
#else VMS
#include <types.h>
#include <time.h>
#endif VMS
/* Macros to fetch individual nibbles. */
#define hinib(x) (((x) >> 4) & 0x0F)
#define lonib(x) ((x) & 0x0F)
/* Macro to restore correct ordering within a byte. */
#define byte(h,l) (((l) << 4)|(h))
#define ADJINT 27
#define TWEAK 50 + 12     /* Fudge factor in centiseconds. */
                         /* CHU code is skewed by 0.5 seconds. */
#define iabs(x) ((x < 0) ? -x : x)
char sample1[5];  /* The first time code. */
char sample2[5];  /* The second (error checking) time code. */
main (argc, argv)
int argc;
char **argv;
{
   char c, *p;
#ifndef VMS
   struct sgttyb sgb;            /* For fiddling with tty line params. */
#endif VMS
   int line;                     /* Fd for the tty line.*/
   int i;
   int adjcnt;                     /* Number of samples before we adjust.*/
   struct tm *localtime(), *ltp, *gmtime();  /* To break out the time. */
   time_t now, time();
#ifdef VMS
   long t0[2], t1[2], t2[2];
#endif VMS
   int amm, ass, mmss, diff;
   char *devstr;
   int adjust;
   int show;
   int tune;
   int daemon;

   setbuf (stdout, NULL);
   adjust = show = tune = 0;
   devstr = "/dev/null";
   for (i = 1; i < argc; i++)
   {
       if (argv[i][0] == '-')
       {
           p = &argv[i][1];
           while (*p)
           {
               switch (*p)
               {
               case 'a':
                   adjust++;
                   break;
               case 't':
                   tune++;
                   break;
               case 's':
                   show++;
                   break;
               case 'd':
                   daemon++;
                   break;
               default:
                   fprintf (stdout, "unknown flag '%c'\n", *p);
                   exit (1);
               }
               *p++;
           }
       }
       else
       {
           strcpy (devstr, argv[i]);
       }
   }
   line = open (devstr, 0);
#ifndef VMS
   /* Set up 8-bit datapath, at 30CPS. */
   gtty (line, &sgb);
   sgb.sg_ispeed = sgb.sg_ospeed = B300;
   sgb.sg_flags |= RAW;
   stty (line, &sgb);
#endif VMS
   adjcnt = 0;
   if (!daemon)
   {
       adjcnt = 19;
   }
   /* Read forever, waiting for the synchronizing BCD 6 digit to appear. */
   for (;;)
   {
       read (line, &c, 1);
       /* We have a syncronizing digit. Grab two samples and compare. */
       if (lonib(c) == 6)
       {
           /* Get first sample. */
           sample1[0] = byte(hinib(c),lonib(c));
           for (i = 1; i < 5; i++)
           {
               read (line, &c, 1);
               sample1[i] = byte(hinib(c),lonib(c));
           }
           /* Get second sample. */
           for (i = 0; i < 5; i++)
           {
               read (line, &c, 1);
               sample2[i] = byte(hinib(c),lonib(c));
           }
           /* If samples match, we have a valid time code. */
           if (compare (sample1, sample2, 5) == 0)
           {
               /* Show the code (if -s).  The high-order nibble in the
                *  first byte is the synch digit, so it gets masked out
                *  for printing.
                */
               if (show)
               {
                   fprintf (stdout, "TC: ");
                   for (i = 0; i < 5; i++)
                   {
                       fprintf (stdout, "%02x", sample1[i]);
                   }
                   fprintf (stdout, "\n");
               }
               if (adjcnt++ >= ADJINT)
               {
                   adjcnt = 0;
                   /* Fetch UTC (GMT). */
                   time (&now);
                   ltp = gmtime (&now);
                   /* Convert time code minutes and seconds into
                    *   binary.
                    */
                   amm = (hinib(sample1[3]) * 10) + lonib(sample1[3]);
                   ass = (hinib(sample1[4]) * 10) + lonib(sample1[4]);
                   /* Convert minutes and seconds portion of system time. */
                   mmss = (ltp->tm_min * 60) + ltp->tm_sec;
                   /* Compute the difference. */
                   diff = ((amm * 60) + ass) - mmss;
                   /* Adjust the system time. */
                   now += (long)diff;
                   if (iabs(diff) > 120)
                   {
                       fprintf (stdout, "%02d-%02d-%02d %02d:%02d:%02d ",
                           ltp->tm_year, ltp->tm_mon + 1, ltp->tm_mday,
                           ltp->tm_hour, ltp->tm_min, ltp->tm_sec);
                       fprintf (stdout, "TERROR %d\n", diff);
                       if (!daemon)
                       {
                           break;
                       }
                       continue;
                   }
                   /* Only do it if there IS a (reasonable) difference. */
                   if ((diff != 0) && (adjust))
                   {
                       ltp = localtime (&now);
                       fprintf (stdout, "%02d-%02d-%02d %02d:%02d:%02d ",
                           ltp->tm_year, ltp->tm_mon + 1, ltp->tm_mday,
                           ltp->tm_hour, ltp->tm_min, ltp->tm_sec);
                       fprintf (stdout, "ADJUST %d\n", diff);
                       /* What we'd REALLY like here is a system call of
                        * the form:   stime (*time, ticks)
                        * that would allow you to set the system tick
                        * counter to <ticks> in centiseconds.  Too bad.
                        *-->stime (&now, TWEAK);<--
                        */
#ifndef VMS
#ifndef EUNICE
                       stime (&now);
#endif EUNICE
#else VMS
                       t1[0] = 100000 * ((diff * 100) + TWEAK);
                       t1[1] = 0;
                       if (diff < 0)
                       {
                           t1[1] = -1;
                       }
                       sys$gettim (t0);
                       lib$addx (t0, t1, t2);
                       sys$setime (t2);
#endif VMS
                   }
                   ltp = localtime (&now);
                   fprintf (stdout, "%02d-%02d-%02d %02d:%02d:%02d TVALID\n",
                       ltp->tm_year, ltp->tm_mon + 1, ltp->tm_mday,
                       ltp->tm_hour, ltp->tm_min, ltp->tm_sec);
                   if (!daemon)
                   {
                       break;
                   }
               }
           }
       }
       else if (tune)
       {
               fprintf (stdout, "FT: %c (%02x)\n", c, (unsigned)c);
       }
   }
}
/* Compare two byte-arrays (s1, s2) of length cnt. */
compare (s1, s2, cnt)
char *s1;
char *s2;
int cnt;
{
   int i;
   for (i = 0; i < cnt; i++)
   {
       if (*s1++ != *s2++)
       {
           return (1);
       }
   }
   return (0);
}
#ifdef VMS
struct tm *
gmtime (tod)
time_t *tod;
{
   int hh;
   int mm;
   char s;
   int secdiff, sgn;
   time_t utc;
   char *p, *getenv ();
   p = getenv ("UTCDISP");
   sscanf (p, "%c %02d:%02d", &s, &hh, &mm);
   utc = *tod;
   secdiff = (hh * 3600) + (mm * 60);
   sgn = (s == '+') ? 1 : -1;
   secdiff *= sgn;
   utc += secdiff;
   return (localtime (&utc));
}
#endif VMS