/*      $NetBSD: refclock_heath.c,v 1.10 2024/08/18 20:47:18 christos Exp $     */

/*
* refclock_heath - clock driver for Heath GC-1000
* (but no longer the GC-1001 Model II, which apparently never worked)
*/

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

#if defined(REFCLOCK) && defined(CLOCK_HEATH)

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

#include <stdio.h>
#include <ctype.h>

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

/*
* This driver supports the Heath GC-1000 Most Accurate Clock, with
* RS232C Output Accessory. This is a WWV/WWVH receiver somewhat less
* robust than other supported receivers. Its claimed accuracy is 100 ms
* when actually synchronized to the broadcast signal, but this doesn't
* happen even most of the time, due to propagation conditions, ambient
* noise sources, etc. When not synchronized, the accuracy is at the
* whim of the internal clock oscillator, which can wander into the
* sunset without warning. Since the indicated precision is 100 ms,
* expect a host synchronized only to this thing to wander to and fro,
* occasionally being rudely stepped when the offset exceeds the default
* clock_max of 128 ms.
*
* There were two GC-1000 versions supported by this driver. The original
* GC-1000 with RS-232 output first appeared in 1983, but dissapeared
* from the market a few years later. The GC-1001 II with RS-232 output
* first appeared circa 1990, but apparently is no longer manufactured.
* The two models differ considerably, both in interface and commands.
* The GC-1000 has a pseudo-bipolar timecode output triggered by a RTS
* transition. The timecode includes both the day of year and time of
* day. The GC-1001 II has a true bipolar output and a complement of
* single character commands. The timecode includes only the time of
* day.
*
* The GC-1001 II was apparently never tested and, based on a Coverity
* scan, apparently never worked [Bug 689].  Related code has been disabled.
*
* GC-1000
*
* The internal DIPswitches should be set to operate in MANUAL mode. The
* external DIPswitches should be set to GMT and 24-hour format.
*
* In MANUAL mode the clock responds to a rising edge of the request to
* send (RTS) modem control line by sending the timecode. Therefore, it
* is necessary that the operating system implement the TIOCMBIC and
* TIOCMBIS ioctl system calls and TIOCM_RTS control bit. Present
* restrictions require the use of a POSIX-compatible programming
* interface, although other interfaces may work as well.
*
* A simple hardware modification to the clock can be made which
* prevents the clock hearing the request to send (RTS) if the HI SPEC
* lamp is out. Route the HISPEC signal to the tone decoder board pin
* 19, from the display, pin 19. Isolate pin 19 of the decoder board
* first, but maintain connection with pin 10. Also isolate pin 38 of
* the CPU on the tone board, and use half an added 7400 to gate the
* original signal to pin 38 with that from pin 19.
*
* The clock message consists of 23 ASCII printing characters in the
* following format:
*
* hh:mm:ss.f AM  dd/mm/yr<cr>
*
*      hh:mm:ss.f = hours, minutes, seconds
*      f = deciseconds ('?' when out of spec)
*      AM/PM/bb = blank in 24-hour mode
*      dd/mm/yr = day, month, year
*
* The alarm condition is indicated by '?', rather than a digit, at f.
* Note that 0?:??:??.? is displayed before synchronization is first
* established and hh:mm:ss.? once synchronization is established and
* then lost again for about a day.
*
* GC-1001 II
*
* Commands consist of a single letter and are case sensitive. When
* enterred in lower case, a description of the action performed is
* displayed. When enterred in upper case the action is performed.
* Following is a summary of descriptions as displayed by the clock:
*
* The clock responds with a command The 'A' command returns an ASCII
* local time string:  HH:MM:SS.T xx<CR>, where
*
*      HH = hours
*      MM = minutes
*      SS = seconds
*      T = tenths-of-seconds
*      xx = 'AM', 'PM', or '  '
*      <CR> = carriage return
*
* The 'D' command returns 24 pairs of bytes containing the variable
* divisor value at the end of each of the previous 24 hours. This
* allows the timebase trimming process to be observed.  UTC hour 00 is
* always returned first. The first byte of each pair is the high byte
* of (variable divisor * 16); the second byte is the low byte of
* (variable divisor * 16). For example, the byte pair 3C 10 would be
* returned for a divisor of 03C1 hex (961 decimal).
*
* The 'I' command returns:  | TH | TL | ER | DH | DL | U1 | I1 | I2 | ,
* where
*
*      TH = minutes since timebase last trimmed (high byte)
*      TL = minutes since timebase last trimmed (low byte)
*      ER = last accumulated error in 1.25 ms increments
*      DH = high byte of (current variable divisor * 16)
*      DL = low byte of (current variable divisor * 16)
*      U1 = UT1 offset (/.1 s):  | + | 4 | 2 | 1 | 0 | 0 | 0 | 0 |
*      I1 = information byte 1:  | W | C | D | I | U | T | Z | 1 | ,
*           where
*
*              W = set by WWV(H)
*              C = CAPTURE LED on
*              D = TRIM DN LED on
*              I = HI SPEC LED on
*              U = TRIM UP LED on
*              T = DST switch on
*              Z = UTC switch on
*              1 = UT1 switch on
*
*      I2 = information byte 2:  | 8 | 8 | 4 | 2 | 1 | D | d | S | ,
*           where
*
*              8, 8, 4, 2, 1 = TIME ZONE switch settings
*              D = DST bit (#55) in last-received frame
*              d = DST bit (#2) in last-received frame
*              S = clock is in simulation mode
*
* The 'P' command returns 24 bytes containing the number of frames
* received without error during UTC hours 00 through 23, providing an
* indication of hourly propagation.  These bytes are updated each hour
* to reflect the previous 24 hour period.  UTC hour 00 is always
* returned first.
*
* The 'T' command returns the UTC time:  | HH | MM | SS | T0 | , where
*      HH = tens-of-hours and hours (packed BCD)
*      MM = tens-of-minutes and minutes (packed BCD)
*      SS = tens-of-seconds and seconds (packed BCD)
*      T = tenths-of-seconds (BCD)
*
* Fudge Factors
*
* A fudge time1 value of .04 s appears to center the clock offset
* residuals. The fudge time2 parameter is the local time offset east of
* Greenwich, which depends on DST. Sorry about that, but the clock
* gives no hint on what the DIPswitches say.
*/

/*
* Interface definitions
*/
#define DEVICE          "/dev/heath%d" /* device name and unit */
#define PRECISION       (-4)    /* precision assumed (about 100 ms) */
#define REFID           "WWV\0" /* reference ID */
#define DESCRIPTION     "Heath GC-1000 Most Accurate Clock" /* WRU */

#define LENHEATH1       23      /* min timecode length */
#if 0   /* BUG 689 */
#define LENHEATH2       13      /* min timecode length */
#endif

/*
* Tables to compute the ddd of year form icky dd/mm timecode. Viva la
* leap.
*/
static int day1tab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
static int day2tab[] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

/*
* Baud rate table. The GC-1000 supports 1200, 2400 and 4800; the
* GC-1001 II supports only 9600.
*/
static int speed[] = {B1200, B2400, B4800, B9600};

/*
* Function prototypes
*/
static  int     heath_start     (int, struct peer *);
static  void    heath_shutdown  (int, struct peer *);
static  void    heath_receive   (struct recvbuf *);
static  void    heath_poll      (int, struct peer *);

/*
* Transfer vector
*/
struct  refclock refclock_heath = {
       heath_start,            /* start up driver */
       heath_shutdown,         /* shut down driver */
       heath_poll,             /* transmit poll message */
       noentry,                /* not used (old heath_control) */
       noentry,                /* initialize driver */
       noentry,                /* not used (old heath_buginfo) */
       NOFLAGS                 /* not used */
};


/*
* heath_start - open the devices and initialize data for processing
*/
static int
heath_start(
       int unit,
       struct peer *peer
       )
{
       struct refclockproc *pp;
       int fd;
       char device[20];

       /*
        * Open serial port
        */
       snprintf(device, sizeof(device), DEVICE, unit);
       fd = refclock_open(&peer->srcadr, device, speed[peer->ttl & 0x3],
                          LDISC_REMOTE);
       if (fd <= 0)
               return (0);
       pp = peer->procptr;
       pp->io.clock_recv = heath_receive;
       pp->io.srcclock = peer;
       pp->io.datalen = 0;
       pp->io.fd = fd;
       if (!io_addclock(&pp->io)) {
               close(fd);
               pp->io.fd = -1;
               return (0);
       }

       /*
        * Initialize miscellaneous variables
        */
       peer->precision = PRECISION;
       pp->clockdesc = DESCRIPTION;
       memcpy(&pp->refid, REFID, 4);
       return (1);
}


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

       pp = peer->procptr;
       if (-1 != pp->io.fd)
               io_closeclock(&pp->io);
}


/*
* heath_receive - receive data from the serial interface
*/
static void
heath_receive(
       struct recvbuf *rbufp
       )
{
       struct refclockproc *pp;
       struct peer *peer;
       l_fp trtmp;
       int month, day;
       int i;
       char dsec, a[5];

       /*
        * Initialize pointers and read the timecode and timestamp
        */
       peer = rbufp->recv_peer;
       pp = peer->procptr;
       pp->lencode = refclock_gtlin(rbufp, pp->a_lastcode, BMAX,
           &trtmp);

       /*
        * We get down to business, check the timecode format and decode
        * its contents. If the timecode has invalid length or is not in
        * proper format, we declare bad format and exit.
        */
       switch (pp->lencode) {

       /*
        * GC-1000 timecode format: "hh:mm:ss.f AM  mm/dd/yy"
        * GC-1001 II timecode format: "hh:mm:ss.f   "
        */
       case LENHEATH1:
               if (sscanf(pp->a_lastcode,
                   "%2d:%2d:%2d.%c%5c%2d/%2d/%2d", &pp->hour,
                   &pp->minute, &pp->second, &dsec, a, &month, &day,
                   &pp->year) != 8) {
                       refclock_report(peer, CEVNT_BADREPLY);
                       return;
               }
               break;

#if 0   /* BUG 689 */
       /*
        * GC-1001 II timecode format: "hh:mm:ss.f   "
        */
       case LENHEATH2:
               if (sscanf(pp->a_lastcode, "%2d:%2d:%2d.%c", &pp->hour,
                   &pp->minute, &pp->second, &dsec) != 4) {
                       refclock_report(peer, CEVNT_BADREPLY);
                       return;
               } else {
                       struct tm *tm_time_p;
                       time_t     now;

                       time(&now);     /* we should grab 'now' earlier */
                       tm_time_p = gmtime(&now);
                       /*
                        * There is a window of time around midnight
                        * where this will Do The Wrong Thing.
                        */
                       if (tm_time_p) {
                               month = tm_time_p->tm_mon + 1;
                               day = tm_time_p->tm_mday;
                       } else {
                               refclock_report(peer, CEVNT_FAULT);
                               return;
                       }
               }
               break;
#endif

       default:
               refclock_report(peer, CEVNT_BADREPLY);
               return;
       }

       /*
        * We determine the day of the year from the DIPswitches. This
        * should be fixed, since somebody might forget to set them.
        * Someday this hazard will be fixed by a fiendish scheme that
        * looks at the timecode and year the radio shows, then computes
        * the residue of the seconds mod the seconds in a leap cycle.
        * If in the third year of that cycle and the third and later
        * months of that year, add one to the day. Then, correct the
        * timecode accordingly. Icky pooh. This bit of nonsense could
        * be avoided if the engineers had been required to write a
        * device driver before finalizing the timecode format.
        */
       if (month < 1 || month > 12 || day < 1) {
               refclock_report(peer, CEVNT_BADTIME);
               return;
       }
       if (pp->year % 4) {
               if (day > day1tab[month - 1]) {
                       refclock_report(peer, CEVNT_BADTIME);
                       return;
               }
               for (i = 0; i < month - 1; i++)
                   day += day1tab[i];
       } else {
               if (day > day2tab[month - 1]) {
                       refclock_report(peer, CEVNT_BADTIME);
                       return;
               }
               for (i = 0; i < month - 1; i++)
                   day += day2tab[i];
       }
       pp->day = day;

       /*
        * Determine synchronization and last update
        */
       if (!isdigit((unsigned char)dsec))
               pp->leap = LEAP_NOTINSYNC;
       else {
               pp->nsec = (dsec - '0') * 100000000;
               pp->leap = LEAP_NOWARNING;
       }
       if (!refclock_process(pp))
               refclock_report(peer, CEVNT_BADTIME);
}


/*
* heath_poll - called by the transmit procedure
*/
static void
heath_poll(
       int unit,
       struct peer *peer
       )
{
       struct refclockproc *pp;
       int bits = TIOCM_RTS;

       /*
        * At each poll we check for timeout and toggle the RTS modem
        * control line, then take a timestamp. Presumably, this is the
        * event the radio captures to generate the timecode.
        * Apparently, the radio takes about a second to make up its
        * mind to send a timecode, so the receive timestamp is
        * worthless.
        */
       pp = peer->procptr;

       /*
        * We toggle the RTS modem control lead (GC-1000) and sent a T
        * (GC-1001 II) to kick a timecode loose from the radio. This
        * code works only for POSIX and SYSV interfaces. With bsd you
        * are on your own. We take a timestamp between the up and down
        * edges to lengthen the pulse, which should be about 50 usec on
        * a Sun IPC. With hotshot CPUs, the pulse might get too short.
        * Later.
        *
        * Bug 689: Even though we no longer support the GC-1001 II,
        * I'm leaving the 'T' write in for timing purposes.
        */
       if (ioctl(pp->io.fd, TIOCMBIC, (char *)&bits) < 0)
               refclock_report(peer, CEVNT_FAULT);
       get_systime(&pp->lastrec);
       if (refclock_write(peer, "T", 1, "T") != 1)
               refclock_report(peer, CEVNT_FAULT);
       ioctl(pp->io.fd, TIOCMBIS, (char *)&bits);
       if (pp->coderecv == pp->codeproc) {
               refclock_report(peer, CEVNT_TIMEOUT);
               return;
       }
       pp->lastref = pp->lastrec;
       refclock_receive(peer);
       record_clock_stats(&peer->srcadr, pp->a_lastcode);
#ifdef DEBUG
       if (debug)
           printf("heath: timecode %d %s\n", pp->lencode,
                  pp->a_lastcode);
#endif
       pp->polls++;
}

#else
NONEMPTY_TRANSLATION_UNIT
#endif /* REFCLOCK */