/*      $NetBSD: refclock_zyfer.c,v 1.6 2024/08/18 20:47:19 christos Exp $      */

/*
* refclock_zyfer - clock driver for the Zyfer GPSTarplus Clock
*
* Harlan Stenn, Jan 2002
*/

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

#if defined(REFCLOCK) && defined(CLOCK_ZYFER)

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

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

#if defined(HAVE_TERMIOS_H)
# include <termios.h>
#elif defined(HAVE_SYS_TERMIOS_H)
# include <sys/termios.h>
#endif
#ifdef HAVE_SYS_PPSCLOCK_H
# include <sys/ppsclock.h>
#endif

/*
* This driver provides support for the TOD serial port of a Zyfer GPStarplus.
* This clock also provides PPS as well as IRIG outputs.
* Precision is limited by the serial driver, etc.
*
* If I was really brave I'd hack/generalize the serial driver to deal
* with arbitrary on-time characters.  This clock *begins* the stream with
* `!`, the on-time character, and the string is *not* EOL-terminated.
*
* Configure the beast for 9600, 8N1.  While I see leap-second stuff
* in the documentation, the published specs on the TOD format only show
* the seconds going to '59'.  I see no leap warning in the TOD format.
*
* The clock sends the following message once per second:
*
*      !TIME,2002,017,07,59,32,2,4,1
*            YYYY DDD HH MM SS m T O
*
*      !               On-time character
*      YYYY            Year
*      DDD     001-366 Day of Year
*      HH      00-23   Hour
*      MM      00-59   Minute
*      SS      00-59   Second (probably 00-60)
*      m       1-5     Time Mode:
*                      1 = GPS time
*                      2 = UTC time
*                      3 = LGPS time (Local GPS)
*                      4 = LUTC time (Local UTC)
*                      5 = Manual time
*      T       4-9     Time Figure Of Merit:
*                      4         x <= 1us
*                      5   1us < x <= 10 us
*                      6  10us < x <= 100us
*                      7 100us < x <= 1ms
*                      8   1ms < x <= 10ms
*                      9  10ms < x
*      O       0-4     Operation Mode:
*                      0 Warm-up
*                      1 Time Locked
*                      2 Coasting
*                      3 Recovering
*                      4 Manual
*
*/

/*
* Interface definitions
*/
#define DEVICE          "/dev/zyfer%d" /* device name and unit */
#define SPEED232        B9600   /* uart speed (9600 baud) */
#define PRECISION       (-20)   /* precision assumed (about 1 us) */
#define REFID           "GPS\0" /* reference ID */
#define DESCRIPTION     "Zyfer GPStarplus" /* WRU */

#define LENZYFER        29      /* timecode length */

/*
* Unit control structure
*/
struct zyferunit {
       u_char  Rcvbuf[LENZYFER + 1];
       u_char  polled;         /* poll message flag */
       int     pollcnt;
       l_fp    tstamp;         /* timestamp of last poll */
       int     Rcvptr;
};

/*
* Function prototypes
*/
static  int     zyfer_start     (int, struct peer *);
static  void    zyfer_shutdown  (int, struct peer *);
static  void    zyfer_receive   (struct recvbuf *);
static  void    zyfer_poll      (int, struct peer *);

/*
* Transfer vector
*/
struct  refclock refclock_zyfer = {
       zyfer_start,            /* start up driver */
       zyfer_shutdown,         /* shut down driver */
       zyfer_poll,             /* transmit poll message */
       noentry,                /* not used (old zyfer_control) */
       noentry,                /* initialize driver (not used) */
       noentry,                /* not used (old zyfer_buginfo) */
       NOFLAGS                 /* not used */
};


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

       /*
        * Open serial port.
        * Something like LDISC_ACTS that looked for ! would be nice...
        */
       snprintf(device, sizeof(device), DEVICE, unit);
       fd = refclock_open(&peer->srcadr, device, SPEED232, LDISC_RAW);
       if (fd <= 0)
               return (0);

       msyslog(LOG_NOTICE, "zyfer(%d) fd: %d dev <%s>", unit, fd, device);

       /*
        * Allocate and initialize unit structure
        */
       up = emalloc(sizeof(struct zyferunit));
       memset(up, 0, sizeof(struct zyferunit));
       pp = peer->procptr;
       pp->io.clock_recv = zyfer_receive;
       pp->io.srcclock = peer;
       pp->io.datalen = 0;
       pp->io.fd = fd;
       if (!io_addclock(&pp->io)) {
               close(fd);
               pp->io.fd = -1;
               free(up);
               return (0);
       }
       pp->unitptr = up;

       /*
        * Initialize miscellaneous variables
        */
       peer->precision = PRECISION;
       pp->clockdesc = DESCRIPTION;
       memcpy((char *)&pp->refid, REFID, 4);
       up->pollcnt = 2;
       up->polled = 0;         /* May not be needed... */

       return (1);
}


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

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


/*
* zyfer_receive - receive data from the serial interface
*/
static void
zyfer_receive(
       struct recvbuf *rbufp
       )
{
       register struct zyferunit *up;
       struct refclockproc *pp;
       struct peer *peer;
       int tmode;              /* Time mode */
       int tfom;               /* Time Figure Of Merit */
       int omode;              /* Operation mode */
       u_char *p;

       TCivilDate      tsdoy;
       TNtpDatum       tsntp;
       l_fp            tfrac;

       peer = rbufp->recv_peer;
       pp = peer->procptr;
       up = pp->unitptr;
       p = (u_char *) &rbufp->recv_space;
       /*
        * If lencode is 0:
        * - if *rbufp->recv_space is !
        * - - call refclock_gtlin to get things going
        * - else flush
        * else stuff it on the end of lastcode
        * If we don't have LENZYFER bytes
        * - wait for more data
        * Crack the beast, and if it's OK, process it.
        *
        * We use refclock_gtlin() because we might use LDISC_CLK.
        *
        * Under FreeBSD, we get the ! followed by two 14-byte packets.
        */

       if (pp->lencode >= LENZYFER)
               pp->lencode = 0;

       if (!pp->lencode) {
               if (*p == '!')
                       pp->lencode = refclock_gtlin(rbufp, pp->a_lastcode,
                                                    BMAX, &pp->lastrec);
               else
                       return;
       } else {
               memcpy(pp->a_lastcode + pp->lencode, p, rbufp->recv_length);
               pp->lencode += rbufp->recv_length;
               pp->a_lastcode[pp->lencode] = '\0';
       }

       if (pp->lencode < LENZYFER)
               return;

       record_clock_stats(&peer->srcadr, pp->a_lastcode);

       /*
        * 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.
        */

       if (pp->lencode != LENZYFER) {
               refclock_report(peer, CEVNT_BADTIME);
               return;
       }

       /*
        * Timecode sample: "!TIME,2002,017,07,59,32,2,4,1"
        */
       if (sscanf(pp->a_lastcode, "!TIME,%4d,%3d,%2d,%2d,%2d,%d,%d,%d",
                  &pp->year, &pp->day, &pp->hour, &pp->minute, &pp->second,
                  &tmode, &tfom, &omode) != 8) {
               refclock_report(peer, CEVNT_BADREPLY);
               return;
       }

       if (tmode != 2) {
               refclock_report(peer, CEVNT_BADTIME);
               return;
       }

       /* Should we make sure tfom is 4? */

       if (omode != 1) {
               pp->leap = LEAP_NOTINSYNC;
               return;
       }

       /* treat GPS input as subject to era warps */
       ZERO(tsdoy);
       ZERO(tfrac);

       tsdoy.year    = pp->year;
       tsdoy.yearday = pp->day;
       tsdoy.hour    = pp->hour;
       tsdoy.minute  = pp->minute;
       tsdoy.second  = pp->second;

       /* note: We kept 'month' and 'monthday' zero above. That forces
        * day-of-year based calculation now:
        */
       tsntp = gpsntp_from_calendar(&tsdoy, tfrac);
       tfrac = ntpfp_from_ntpdatum(&tsntp);
       refclock_process_offset(pp, tfrac, pp->lastrec, pp->fudgetime1);

       /*
        * Good place for record_clock_stats()
        */
       up->pollcnt = 2;

       if (up->polled) {
               up->polled = 0;
               refclock_receive(peer);
       }
}


/*
* zyfer_poll - called by the transmit procedure
*/
static void
zyfer_poll(
       int unit,
       struct peer *peer
       )
{
       register struct zyferunit *up;
       struct refclockproc *pp;

       /*
        * We don't really do anything here, except arm the receiving
        * side to capture a sample and check for timeouts.
        */
       pp = peer->procptr;
       up = pp->unitptr;
       if (!up->pollcnt)
               refclock_report(peer, CEVNT_TIMEOUT);
       else
               up->pollcnt--;
       pp->polls++;
       up->polled = 1;
}

#else
NONEMPTY_TRANSLATION_UNIT
#endif /* REFCLOCK */