refclock_neoclock4x.c   [plain text]


/*
 *
 * Refclock_neoclock4x.c
 * - NeoClock4X driver for DCF77 or FIA Timecode
 *
 * Date: 2006-01-11 v1.15
 *
 * see http://www.linum.com/redir/jump/id=neoclock4x&action=redir
 * for details about the NeoClock4X device
 *
 * Copyright (C) 2002-2004 by Linum Software GmbH <neoclock4x@linum.com>
 *
 * 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.
 *
 *
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#if defined(REFCLOCK) && (defined(CLOCK_NEOCLOCK4X))

#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <ctype.h>

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

#if defined HAVE_SYS_MODEM_H
# include <sys/modem.h>
# ifndef __QNXNTO__
#  define TIOCMSET MCSETA
#  define TIOCMGET MCGETA
#  define TIOCM_RTS MRTS
# endif
#endif

#ifdef HAVE_TERMIOS_H
# ifdef TERMIOS_NEEDS__SVID3
#  define _SVID3
# endif
# include <termios.h>
# ifdef TERMIOS_NEEDS__SVID3
#  undef _SVID3
# endif
#endif

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

/*
 * NTP version 4.20 change the pp->msec field to pp->nsec.
 * To allow to support older ntp versions with this sourcefile
 * you can define NTP_PRE_420 to allow this driver to compile
 * with ntp version back to 4.1.2.
 *
 */
#if 0
#define NTP_PRE_420
#endif

/*
 * If you want the driver for whatever reason to not use
 * the TX line to send anything to your NeoClock4X
 * device you must tell the NTP refclock driver which
 * firmware you NeoClock4X device uses.
 *
 * If you want to enable this feature change the "#if 0"
 * line to "#if 1" and make sure that the defined firmware
 * matches the firmware off your NeoClock4X receiver!
 *
 */

#if 0
#define NEOCLOCK4X_FIRMWARE                NEOCLOCK4X_FIRMWARE_VERSION_A
#endif

/* at this time only firmware version A is known */
#define NEOCLOCK4X_FIRMWARE_VERSION_A      'A'

#define NEOCLOCK4X_TIMECODELEN 37

#define NEOCLOCK4X_OFFSET_SERIAL            3
#define NEOCLOCK4X_OFFSET_RADIOSIGNAL       9
#define NEOCLOCK4X_OFFSET_DAY              12
#define NEOCLOCK4X_OFFSET_MONTH            14
#define NEOCLOCK4X_OFFSET_YEAR             16
#define NEOCLOCK4X_OFFSET_HOUR             18
#define NEOCLOCK4X_OFFSET_MINUTE           20
#define NEOCLOCK4X_OFFSET_SECOND           22
#define NEOCLOCK4X_OFFSET_HSEC             24
#define NEOCLOCK4X_OFFSET_DOW              26
#define NEOCLOCK4X_OFFSET_TIMESOURCE       28
#define NEOCLOCK4X_OFFSET_DSTSTATUS        29
#define NEOCLOCK4X_OFFSET_QUARZSTATUS      30
#define NEOCLOCK4X_OFFSET_ANTENNA1         31
#define NEOCLOCK4X_OFFSET_ANTENNA2         33
#define NEOCLOCK4X_OFFSET_CRC              35

#define NEOCLOCK4X_DRIVER_VERSION          "1.15 (2006-01-11)"

#define NSEC_TO_MILLI                      1000000

struct neoclock4x_unit {
  l_fp	laststamp;	/* last receive timestamp */
  short	unit;		/* NTP refclock unit number */
  u_long polled;	/* flag to detect noreplies */
  char	leap_status;	/* leap second flag */
  int	recvnow;

  char  firmware[80];
  char  firmwaretag;
  char  serial[7];
  char  radiosignal[4];
  char  timesource;
  char  dststatus;
  char  quarzstatus;
  int   antenna1;
  int   antenna2;
  int   utc_year;
  int   utc_month;
  int   utc_day;
  int   utc_hour;
  int   utc_minute;
  int   utc_second;
  int   utc_msec;
};

static	int	neoclock4x_start        (int, struct peer *);
static	void	neoclock4x_shutdown	(int, struct peer *);
static	void	neoclock4x_receive	(struct recvbuf *);
static	void	neoclock4x_poll		(int, struct peer *);
static	void	neoclock4x_control      (int, struct refclockstat *, struct refclockstat *, struct peer *);

static int      neol_atoi_len           (const char str[], int *, int);
static int      neol_hexatoi_len        (const char str[], int *, int);
static void     neol_jdn_to_ymd         (unsigned long, int *, int *, int *);
static void     neol_localtime          (unsigned long, int* , int*, int*, int*, int*, int*);
static unsigned long neol_mktime        (int, int, int, int, int, int);
#if !defined(NEOCLOCK4X_FIRMWARE)
static int      neol_query_firmware     (int, int, char *, int);
static int      neol_check_firmware     (int, const char*, char *);
#endif

struct refclock refclock_neoclock4x = {
  neoclock4x_start,	/* start up driver */
  neoclock4x_shutdown,	/* shut down driver */
  neoclock4x_poll,	/* transmit poll message */
  neoclock4x_control,
  noentry,		/* initialize driver (not used) */
  noentry,		/* not used */
  NOFLAGS			/* not used */
};

static int
neoclock4x_start(int unit,
		 struct peer *peer)
{
  struct neoclock4x_unit *up;
  struct refclockproc *pp;
  int fd;
  char dev[20];
  int sl232;
#if defined(HAVE_TERMIOS)
  struct termios termsettings;
#endif
#if !defined(NEOCLOCK4X_FIRMWARE)
  int tries;
#endif

  (void) snprintf(dev, sizeof(dev)-1, "/dev/neoclock4x-%d", unit);

  /* LDISC_STD, LDISC_RAW
   * Open serial port. Use CLK line discipline, if available.
   */
  fd = refclock_open(dev, B2400, LDISC_STD);
  if(fd <= 0)
    {
      return (0);
    }

#if defined(HAVE_TERMIOS)

#if 1
  if(tcgetattr(fd, &termsettings) < 0)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): (tcgetattr) can't query serial port settings: %m", unit);
      (void) close(fd);
      return (0);
    }

  /* 2400 Baud 8N2 */
  termsettings.c_iflag = IGNBRK | IGNPAR | ICRNL;
  termsettings.c_oflag = 0;
  termsettings.c_cflag = CS8 | CSTOPB | CLOCAL | CREAD;
  (void)cfsetispeed(&termsettings, (u_int)B2400);
  (void)cfsetospeed(&termsettings, (u_int)B2400);

  if(tcsetattr(fd, TCSANOW, &termsettings) < 0)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): (tcsetattr) can't set serial port 2400 8N2: %m", unit);
      (void) close(fd);
      return (0);
    }

#else
  if(tcgetattr(fd, &termsettings) < 0)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): (tcgetattr) can't query serial port settings: %m", unit);
      (void) close(fd);
      return (0);
    }

  /* 2400 Baud 8N2 */
  termsettings.c_cflag &= ~PARENB;
  termsettings.c_cflag |= CSTOPB;
  termsettings.c_cflag &= ~CSIZE;
  termsettings.c_cflag |= CS8;

  if(tcsetattr(fd, TCSANOW, &termsettings) < 0)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): (tcsetattr) can't set serial port 2400 8N2: %m", unit);
      (void) close(fd);
      return (0);
    }
#endif

#elif defined(HAVE_SYSV_TTYS)
  if(ioctl(fd, TCGETA, &termsettings) < 0)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): (TCGETA) can't query serial port settings: %m", unit);
      (void) close(fd);
      return (0);
    }

  /* 2400 Baud 8N2 */
  termsettings.c_cflag &= ~PARENB;
  termsettings.c_cflag |= CSTOPB;
  termsettings.c_cflag &= ~CSIZE;
  termsettings.c_cflag |= CS8;

  if(ioctl(fd, TCSETA, &termsettings) < 0)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): (TSGETA) can't set serial port 2400 8N2: %m", unit);
      (void) close(fd);
      return (0);
    }
#else
  msyslog(LOG_EMERG, "NeoClock4X(%d): don't know how to set port to 2400 8N2 with this OS!", unit);
  (void) close(fd);
  return (0);
#endif

#if defined(TIOCMSET) && (defined(TIOCM_RTS) || defined(CIOCM_RTS))
  /* turn on RTS, and DTR for power supply */
  /* NeoClock4x is powered from serial line */
  if(ioctl(fd, TIOCMGET, (caddr_t)&sl232) == -1)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): can't query RTS/DTR state: %m", unit);
      (void) close(fd);
      return (0);
    }
#ifdef TIOCM_RTS
  sl232 = sl232 | TIOCM_DTR | TIOCM_RTS;	/* turn on RTS, and DTR for power supply */
#else
  sl232 = sl232 | CIOCM_DTR | CIOCM_RTS;	/* turn on RTS, and DTR for power supply */
#endif
  if(ioctl(fd, TIOCMSET, (caddr_t)&sl232) == -1)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): can't set RTS/DTR to power neoclock4x: %m", unit);
      (void) close(fd);
      return (0);
    }
#else
  msyslog(LOG_EMERG, "NeoClock4X(%d): don't know how to set DTR/RTS to power NeoClock4X with this OS!",
	  unit);
  (void) close(fd);
  return (0);
#endif

  up = (struct neoclock4x_unit *) emalloc(sizeof(struct neoclock4x_unit));
  if(!(up))
    {
      msyslog(LOG_ERR, "NeoClock4X(%d): can't allocate memory for: %m",unit);
      (void) close(fd);
      return (0);
    }

  memset((char *)up, 0, sizeof(struct neoclock4x_unit));
  pp = peer->procptr;
  pp->clockdesc = "NeoClock4X";
  pp->unitptr = (caddr_t)up;
  pp->io.clock_recv = neoclock4x_receive;
  pp->io.srcclock = (caddr_t)peer;
  pp->io.datalen = 0;
  pp->io.fd = fd;
  /*
   * no fudge time is given by user!
   * use 169.583333 ms to compensate the serial line delay
   * formula is:
   * 2400 Baud / 11 bit = 218.18 charaters per second
   *  (NeoClock4X timecode len)
   */
  pp->fudgetime1 = (NEOCLOCK4X_TIMECODELEN * 11) / 2400.0;

  /*
   * Initialize miscellaneous variables
   */
  peer->precision = -10;
  peer->burst = NSTAGE;
  memcpy((char *)&pp->refid, "neol", 4);

  up->leap_status = 0;
  up->unit = unit;
  strcpy(up->firmware, "?");
  up->firmwaretag = '?';
  strcpy(up->serial, "?");
  strcpy(up->radiosignal, "?");
  up->timesource  = '?';
  up->dststatus   = '?';
  up->quarzstatus = '?';
  up->antenna1    = -1;
  up->antenna2    = -1;
  up->utc_year    = 0;
  up->utc_month   = 0;
  up->utc_day     = 0;
  up->utc_hour    = 0;
  up->utc_minute  = 0;
  up->utc_second  = 0;
  up->utc_msec    = 0;

#if defined(NEOCLOCK4X_FIRMWARE)
#if NEOCLOCK4X_FIRMWARE == NEOCLOCK4X_FIRMWARE_VERSION_A
  strcpy(up->firmware, "(c) 2002 NEOL S.A. FRANCE / L0.01 NDF:A:* (compile time)");
  up->firmwaretag = 'A';
#else
  msyslog(LOG_EMERG, "NeoClock4X(%d): unknown firmware defined at compile time for NeoClock4X",
	  unit);
  (void) close(fd);
  pp->io.fd = -1;
  free(pp->unitptr);
  pp->unitptr = NULL;
  return (0);
#endif
#else
  for(tries=0; tries < 5; tries++)
    {
      NLOG(NLOG_CLOCKINFO)
	msyslog(LOG_INFO, "NeoClock4X(%d): checking NeoClock4X firmware version (%d/5)", unit, tries);
      /* wait 3 seconds for receiver to power up */
      sleep(3);
      if(neol_query_firmware(pp->io.fd, up->unit, up->firmware, sizeof(up->firmware)))
	{
	  break;
	}
    }

  /* can I handle this firmware version? */
  if(!neol_check_firmware(up->unit, up->firmware, &up->firmwaretag))
    {
      (void) close(fd);
      pp->io.fd = -1;
      free(pp->unitptr);
      pp->unitptr = NULL;
      return (0);
    }
#endif

  if(!io_addclock(&pp->io))
    {
      msyslog(LOG_ERR, "NeoClock4X(%d): error add peer to ntpd: %m", unit);
      (void) close(fd);
      pp->io.fd = -1;
      free(pp->unitptr);
      pp->unitptr = NULL;
      return (0);
    }

  NLOG(NLOG_CLOCKINFO)
    msyslog(LOG_INFO, "NeoClock4X(%d): receiver setup successful done", unit);

  return (1);
}

static void
neoclock4x_shutdown(int unit,
		   struct peer *peer)
{
  struct neoclock4x_unit *up;
  struct refclockproc *pp;
  int sl232;

  if(NULL != peer)
    {
      pp = peer->procptr;
      if(pp != NULL)
        {
          up = (struct neoclock4x_unit *)pp->unitptr;
          if(up != NULL)
            {
              if(-1 !=  pp->io.fd)
                {
#if defined(TIOCMSET) && (defined(TIOCM_RTS) || defined(CIOCM_RTS))
                  /* turn on RTS, and DTR for power supply */
                  /* NeoClock4x is powered from serial line */
                  if(ioctl(pp->io.fd, TIOCMGET, (caddr_t)&sl232) == -1)
                    {
                      msyslog(LOG_CRIT, "NeoClock4X(%d): can't query RTS/DTR state: %m",
                              unit);
                    }
#ifdef TIOCM_RTS
                  /* turn on RTS, and DTR for power supply */
                  sl232 &= ~(TIOCM_DTR | TIOCM_RTS);
#else
                  /* turn on RTS, and DTR for power supply */
                  sl232 &= ~(CIOCM_DTR | CIOCM_RTS);
#endif
                  if(ioctl(pp->io.fd, TIOCMSET, (caddr_t)&sl232) == -1)
                    {
                      msyslog(LOG_CRIT, "NeoClock4X(%d): can't set RTS/DTR to power neoclock4x: %m",
                              unit);
                    }
#endif
                  io_closeclock(&pp->io);
                }
              free(up);
              pp->unitptr = NULL;
            }
        }
    }

  msyslog(LOG_ERR, "NeoClock4X(%d): shutdown", unit);

  NLOG(NLOG_CLOCKINFO)
    msyslog(LOG_INFO, "NeoClock4X(%d): receiver shutdown done", unit);
}

static void
neoclock4x_receive(struct recvbuf *rbufp)
{
  struct neoclock4x_unit *up;
  struct refclockproc *pp;
  struct peer *peer;
  unsigned long calc_utc;
  int day;
  int month;	/* ddd conversion */
  int c;
  int dsec;
  unsigned char calc_chksum;
  int recv_chksum;

  peer = (struct peer *)rbufp->recv_srcclock;
  pp = peer->procptr;
  up = (struct neoclock4x_unit *)pp->unitptr;

  /* wait till poll interval is reached */
  if(0 == up->recvnow)
    return;

  /* reset poll interval flag */
  up->recvnow = 0;

  /* read last received timecode */
  pp->lencode = refclock_gtlin(rbufp, pp->a_lastcode, BMAX, &pp->lastrec);
  pp->leap = LEAP_NOWARNING;

  if(NEOCLOCK4X_TIMECODELEN != pp->lencode)
    {
      NLOG(NLOG_CLOCKEVENT)
	msyslog(LOG_WARNING, "NeoClock4X(%d): received data has invalid length, expected %d bytes, received %d bytes: %s",
		up->unit, NEOCLOCK4X_TIMECODELEN, pp->lencode, pp->a_lastcode);
      refclock_report(peer, CEVNT_BADREPLY);
      return;
    }

  neol_hexatoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_CRC], &recv_chksum, 2);

  /* calculate checksum */
  calc_chksum = 0;
  for(c=0; c < NEOCLOCK4X_OFFSET_CRC; c++)
    {
      calc_chksum += pp->a_lastcode[c];
    }
  if(recv_chksum != calc_chksum)
    {
      NLOG(NLOG_CLOCKEVENT)
	msyslog(LOG_WARNING, "NeoClock4X(%d): received data has invalid chksum: %s",
		up->unit, pp->a_lastcode);
      refclock_report(peer, CEVNT_BADREPLY);
      return;
    }

  /* Allow synchronization even is quartz clock is
   * never initialized.
   * WARNING: This is dangerous!
   */
  up->quarzstatus = pp->a_lastcode[NEOCLOCK4X_OFFSET_QUARZSTATUS];
  if(0==(pp->sloppyclockflag & CLK_FLAG2))
    {
      if('I' != up->quarzstatus)
	{
	  NLOG(NLOG_CLOCKEVENT)
	    msyslog(LOG_NOTICE, "NeoClock4X(%d): quartz clock is not initialized: %s",
		    up->unit, pp->a_lastcode);
	  pp->leap = LEAP_NOTINSYNC;
	  refclock_report(peer, CEVNT_BADDATE);
	  return;
	}
    }
  if('I' != up->quarzstatus)
    {
      NLOG(NLOG_CLOCKEVENT)
	msyslog(LOG_NOTICE, "NeoClock4X(%d): using uninitialized quartz clock for time synchronization: %s",
		up->unit, pp->a_lastcode);
    }

  /*
   * If NeoClock4X is not synchronized to a radio clock
   * check if we're allowed to synchronize with the quartz
   * clock.
   */
  up->timesource = pp->a_lastcode[NEOCLOCK4X_OFFSET_TIMESOURCE];
  if(0==(pp->sloppyclockflag & CLK_FLAG2))
    {
      if('A' != up->timesource)
	{
	  /* not allowed to sync with quartz clock */
	  if(0==(pp->sloppyclockflag & CLK_FLAG1))
	    {
	      refclock_report(peer, CEVNT_BADTIME);
	      pp->leap = LEAP_NOTINSYNC;
	      return;
	    }
	}
    }

  /* this should only used when first install is done */
  if(pp->sloppyclockflag & CLK_FLAG4)
    {
      msyslog(LOG_DEBUG, "NeoClock4X(%d): received data: %s",
	      up->unit, pp->a_lastcode);
    }

  /* 123456789012345678901234567890123456789012345 */
  /* S/N123456DCF1004021010001202ASX1213CR\r\n */

  neol_atoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_YEAR], &pp->year, 2);
  neol_atoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_MONTH], &month, 2);
  neol_atoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_DAY], &day, 2);
  neol_atoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_HOUR], &pp->hour, 2);
  neol_atoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_MINUTE], &pp->minute, 2);
  neol_atoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_SECOND], &pp->second, 2);
  neol_atoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_HSEC], &dsec, 2);
#if defined(NTP_PRE_420)
  pp->msec = dsec * 10; /* convert 1/100s from neoclock to real miliseconds */
#else
  pp->nsec = dsec * 10 * NSEC_TO_MILLI; /* convert 1/100s from neoclock to nanoseconds */
#endif

  memcpy(up->radiosignal, &pp->a_lastcode[NEOCLOCK4X_OFFSET_RADIOSIGNAL], 3);
  up->radiosignal[3] = 0;
  memcpy(up->serial, &pp->a_lastcode[NEOCLOCK4X_OFFSET_SERIAL], 6);
  up->serial[6] = 0;
  up->dststatus = pp->a_lastcode[NEOCLOCK4X_OFFSET_DSTSTATUS];
  neol_hexatoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_ANTENNA1], &up->antenna1, 2);
  neol_hexatoi_len(&pp->a_lastcode[NEOCLOCK4X_OFFSET_ANTENNA2], &up->antenna2, 2);

  /*
    Validate received values at least enough to prevent internal
    array-bounds problems, etc.
  */
  if((pp->hour < 0) || (pp->hour > 23) ||
     (pp->minute < 0) || (pp->minute > 59) ||
     (pp->second < 0) || (pp->second > 60) /*Allow for leap seconds.*/ ||
     (day < 1) || (day > 31) ||
     (month < 1) || (month > 12) ||
     (pp->year < 0) || (pp->year > 99)) {
    /* Data out of range. */
    NLOG(NLOG_CLOCKEVENT)
      msyslog(LOG_WARNING, "NeoClock4X(%d): date/time out of range: %s",
	      up->unit, pp->a_lastcode);
    refclock_report(peer, CEVNT_BADDATE);
    return;
  }

  /* Year-2000 check not needed anymore. Same problem
   * will arise at 2099 but what should we do...?
   *
   * wrap 2-digit date into 4-digit
   *
   * if(pp->year < YEAR_PIVOT)
   * {
   *   pp->year += 100;
   * }
  */
  pp->year += 2000;

  /* adjust NeoClock4X local time to UTC */
  calc_utc = neol_mktime(pp->year, month, day, pp->hour, pp->minute, pp->second);
  calc_utc -= 3600;
  /* adjust NeoClock4X daylight saving time if needed */
  if('S' == up->dststatus)
    calc_utc -= 3600;
  neol_localtime(calc_utc, &pp->year, &month, &day, &pp->hour, &pp->minute, &pp->second);

  /*
    some preparations
  */
  pp->day = ymd2yd(pp->year, month, day);
  pp->leap = 0;

  if(pp->sloppyclockflag & CLK_FLAG4)
    {
      msyslog(LOG_DEBUG, "NeoClock4X(%d): calculated UTC date/time: %04d-%02d-%02d %02d:%02d:%02d.%03ld",
	      up->unit,
	      pp->year, month, day,
	      pp->hour, pp->minute, pp->second,
#if defined(NTP_PRE_420)
              pp->msec
#else
              pp->nsec/NSEC_TO_MILLI
#endif
              );
    }

  up->utc_year   = pp->year;
  up->utc_month  = month;
  up->utc_day    = day;
  up->utc_hour   = pp->hour;
  up->utc_minute = pp->minute;
  up->utc_second = pp->second;
#if defined(NTP_PRE_420)
  up->utc_msec   = pp->msec;
#else
  up->utc_msec   = pp->nsec/NSEC_TO_MILLI;
#endif

  if(!refclock_process(pp))
    {
      NLOG(NLOG_CLOCKEVENT)
	msyslog(LOG_WARNING, "NeoClock4X(%d): refclock_process failed!", up->unit);
      refclock_report(peer, CEVNT_FAULT);
      return;
    }
  refclock_receive(peer);

  /* report good status */
  refclock_report(peer, CEVNT_NOMINAL);

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

static void
neoclock4x_poll(int unit,
		struct peer *peer)
{
  struct neoclock4x_unit *up;
  struct refclockproc *pp;

  pp = peer->procptr;
  up = (struct neoclock4x_unit *)pp->unitptr;

  pp->polls++;
  up->recvnow = 1;
}

static void
neoclock4x_control(int unit,
		   struct refclockstat *in,
		   struct refclockstat *out,
		   struct peer *peer)
{
  struct neoclock4x_unit *up;
  struct refclockproc *pp;

  if(NULL == peer)
    {
      msyslog(LOG_ERR, "NeoClock4X(%d): control: unit invalid/inactive", unit);
      return;
    }

  pp = peer->procptr;
  if(NULL == pp)
    {
      msyslog(LOG_ERR, "NeoClock4X(%d): control: unit invalid/inactive", unit);
      return;
    }

  up = (struct neoclock4x_unit *)pp->unitptr;
  if(NULL == up)
    {
      msyslog(LOG_ERR, "NeoClock4X(%d): control: unit invalid/inactive", unit);
      return;
    }

  if(NULL != in)
    {
      /* check to see if a user supplied time offset is given */
      if(in->haveflags & CLK_HAVETIME1)
	{
	  pp->fudgetime1 = in->fudgetime1;
	  NLOG(NLOG_CLOCKINFO)
	    msyslog(LOG_NOTICE, "NeoClock4X(%d): using fudgetime1 with %0.5fs from ntp.conf.",
		    unit, pp->fudgetime1);
	}

      /* notify */
      if(pp->sloppyclockflag & CLK_FLAG1)
	{
	  NLOG(NLOG_CLOCKINFO)
	    msyslog(LOG_NOTICE, "NeoClock4X(%d): quartz clock is used to synchronize time if radio clock has no reception.", unit);
	}
      else
	{
	  NLOG(NLOG_CLOCKINFO)
	    msyslog(LOG_NOTICE, "NeoClock4X(%d): time is only adjusted with radio signal reception.", unit);
	}
    }

  if(NULL != out)
    {
      char *tt;
      char tmpbuf[80];

      out->kv_list = (struct ctl_var *)0;
      out->type    = REFCLK_NEOCLOCK4X;

      snprintf(tmpbuf, sizeof(tmpbuf)-1,
	       "%04d-%02d-%02d %02d:%02d:%02d.%03d",
	       up->utc_year, up->utc_month, up->utc_day,
	       up->utc_hour, up->utc_minute, up->utc_second,
	       up->utc_msec);
      tt = add_var(&out->kv_list, sizeof(tmpbuf)-1, RO|DEF);
      snprintf(tt, sizeof(tmpbuf)-1, "calc_utc=\"%s\"", tmpbuf);

      tt = add_var(&out->kv_list, 40, RO|DEF);
      snprintf(tt, 39, "radiosignal=\"%s\"", up->radiosignal);
      tt = add_var(&out->kv_list, 40, RO|DEF);
      snprintf(tt, 39, "antenna1=\"%d\"", up->antenna1);
      tt = add_var(&out->kv_list, 40, RO|DEF);
      snprintf(tt, 39, "antenna2=\"%d\"", up->antenna2);
      tt = add_var(&out->kv_list, 40, RO|DEF);
      if('A' == up->timesource)
	snprintf(tt, 39, "timesource=\"radio\"");
      else if('C' == up->timesource)
	snprintf(tt, 39, "timesource=\"quartz\"");
      else
	snprintf(tt, 39, "timesource=\"unknown\"");
      tt = add_var(&out->kv_list, 40, RO|DEF);
      if('I' == up->quarzstatus)
	snprintf(tt, 39, "quartzstatus=\"synchronized\"");
      else if('X' == up->quarzstatus)
        snprintf(tt, 39, "quartzstatus=\"not synchronized\"");
      else
	snprintf(tt, 39, "quartzstatus=\"unknown\"");
      tt = add_var(&out->kv_list, 40, RO|DEF);
      if('S' == up->dststatus)
        snprintf(tt, 39, "dststatus=\"summer\"");
      else if('W' == up->dststatus)
        snprintf(tt, 39, "dststatus=\"winter\"");
      else
        snprintf(tt, 39, "dststatus=\"unknown\"");
      tt = add_var(&out->kv_list, 80, RO|DEF);
      snprintf(tt, 79, "firmware=\"%s\"", up->firmware);
      tt = add_var(&out->kv_list, 40, RO|DEF);
      snprintf(tt, 39, "firmwaretag=\"%c\"", up->firmwaretag);
      tt = add_var(&out->kv_list, 80, RO|DEF);
      snprintf(tt, 79, "driver version=\"%s\"", NEOCLOCK4X_DRIVER_VERSION);
      tt = add_var(&out->kv_list, 80, RO|DEF);
      snprintf(tt, 79, "serialnumber=\"%s\"", up->serial);
    }
}

static int
neol_hexatoi_len(const char str[],
		 int *result,
		 int maxlen)
{
  int hexdigit;
  int i;
  int n = 0;

  for(i=0; isxdigit((int)str[i]) && i < maxlen; i++)
    {
      hexdigit = isdigit((int)str[i]) ? toupper(str[i]) - '0' : toupper(str[i]) - 'A' + 10;
      n = 16 * n + hexdigit;
    }
  *result = n;
  return (n);
}

static int
neol_atoi_len(const char str[],
		  int *result,
		  int maxlen)
{
  int digit;
  int i;
  int n = 0;

  for(i=0; isdigit((int)str[i]) && i < maxlen; i++)
    {
      digit = str[i] - '0';
      n = 10 * n + digit;
    }
  *result = n;
  return (n);
}

/* Converts Gregorian date to seconds since 1970-01-01 00:00:00.
 * Assumes input in normal date format, i.e. 1980-12-31 23:59:59
 * => year=1980, mon=12, day=31, hour=23, min=59, sec=59.
 *
 * [For the Julian calendar (which was used in Russia before 1917,
 * Britain & colonies before 1752, anywhere else before 1582,
 * and is still in use by some communities) leave out the
 * -year/100+year/400 terms, and add 10.]
 *
 * This algorithm was first published by Gauss (I think).
 *
 * WARNING: this function will overflow on 2106-02-07 06:28:16 on
 * machines were long is 32-bit! (However, as time_t is signed, we
 * will already get problems at other places on 2038-01-19 03:14:08)
 */
static unsigned long
neol_mktime(int year,
	    int mon,
	    int day,
	    int hour,
	    int min,
	    int sec)
{
  if (0 >= (int) (mon -= 2)) {    /* 1..12 . 11,12,1..10 */
    mon += 12;      /* Puts Feb last since it has leap day */
    year -= 1;
  }
  return (((
            (unsigned long)(year/4 - year/100 + year/400 + 367*mon/12 + day) +
            year*365 - 719499
            )*24 + hour /* now have hours */
           )*60 + min /* now have minutes */
          )*60 + sec; /* finally seconds */
}

static void
neol_localtime(unsigned long utc,
	       int* year,
	       int* month,
	       int* day,
	       int* hour,
	       int* min,
	       int* sec)
{
  *sec = utc % 60;
  utc /= 60;
  *min = utc % 60;
  utc /= 60;
  *hour = utc % 24;
  utc /= 24;

  /*             JDN Date 1/1/1970 */
  neol_jdn_to_ymd(utc + 2440588L, year, month, day);
}

static void
neol_jdn_to_ymd(unsigned long jdn,
		int *yy,
		int *mm,
		int *dd)
{
  unsigned long x, z, m, d, y;
  unsigned long daysPer400Years = 146097UL;
  unsigned long fudgedDaysPer4000Years = 1460970UL + 31UL;

  x = jdn + 68569UL;
  z = 4UL * x / daysPer400Years;
  x = x - (daysPer400Years * z + 3UL) / 4UL;
  y = 4000UL * (x + 1) / fudgedDaysPer4000Years;
  x = x - 1461UL * y / 4UL + 31UL;
  m = 80UL * x / 2447UL;
  d = x - 2447UL * m / 80UL;
  x = m / 11UL;
  m = m + 2UL - 12UL * x;
  y = 100UL * (z - 49UL) + y + x;

  *yy = (int)y;
  *mm = (int)m;
  *dd = (int)d;
}

#if !defined(NEOCLOCK4X_FIRMWARE)
static int
neol_query_firmware(int fd,
		    int unit,
		    char *firmware,
		    int maxlen)
{
  char tmpbuf[256];
  int len;
  int lastsearch;
  unsigned char c;
  int last_c_was_crlf;
  int last_crlf_conv_len;
  int init;
  int read_errors;
  int flag = 0;
  int chars_read;

  /* wait a little bit */
  sleep(1);
  if(-1 != write(fd, "V", 1))
    {
      /* wait a little bit */
      sleep(1);
      memset(tmpbuf, 0x00, sizeof(tmpbuf));

      len = 0;
      lastsearch = 0;
      last_c_was_crlf = 0;
      last_crlf_conv_len = 0;
      init = 1;
      read_errors = 0;
      chars_read = 0;
      for(;;)
	{
	  if(read_errors > 5)
	    {
	      msyslog(LOG_ERR, "NeoClock4X(%d): can't read firmware version (timeout)", unit);
	      strcpy(tmpbuf, "unknown due to timeout");
	      break;
	    }
          if(chars_read > 500)
            {
	      msyslog(LOG_ERR, "NeoClock4X(%d): can't read firmware version (garbage)", unit);
	      strcpy(tmpbuf, "unknown due to garbage input");
	      break;
            }
	  if(-1 == read(fd, &c, 1))
	    {
              if(EAGAIN != errno)
                {
                  msyslog(LOG_DEBUG, "NeoClock4x(%d): read: %s", unit ,strerror(errno));
                  read_errors++;
                }
              else
                {
                  sleep(1);
                }
	      continue;
	    }
          else
            {
              chars_read++;
            }

	  if(init)
	    {
	      if(0xA9 != c) /* wait for (c) char in input stream */
		continue;

	      strcpy(tmpbuf, "(c)");
	      len = 3;
	      init = 0;
	      continue;
	    }

#if 0
	  msyslog(LOG_NOTICE, "NeoClock4X(%d): firmware %c = %02Xh", unit, c, c);
#endif

	  if(0x0A == c || 0x0D == c)
	    {
	      if(last_c_was_crlf)
		{
		  char *ptr;
		  ptr = strstr(&tmpbuf[lastsearch], "S/N");
		  if(NULL != ptr)
		    {
		      tmpbuf[last_crlf_conv_len] = 0;
		      flag = 1;
		      break;
		    }
		  /* convert \n to / */
		  last_crlf_conv_len = len;
		  tmpbuf[len++] = ' ';
		  tmpbuf[len++] = '/';
		  tmpbuf[len++] = ' ';
		  lastsearch = len;
		}
	      last_c_was_crlf = 1;
	    }
	  else
	    {
	      last_c_was_crlf = 0;
	      if(0x00 != c)
		tmpbuf[len++] = (char) c;
	    }
	  tmpbuf[len] = '\0';
	  if(len > sizeof(tmpbuf)-5)
	    break;
	}
    }
  else
    {
      msyslog(LOG_ERR, "NeoClock4X(%d): can't query firmware version", unit);
      strcpy(tmpbuf, "unknown error");
    }
  strncpy(firmware, tmpbuf, maxlen);
  firmware[maxlen] = '\0';

  if(flag)
    {
      NLOG(NLOG_CLOCKINFO)
	msyslog(LOG_INFO, "NeoClock4X(%d): firmware version: %s", unit, firmware);
    }

  return (flag);
}

static int
neol_check_firmware(int unit,
                    const char *firmware,
                    char *firmwaretag)
{
  char *ptr;

  *firmwaretag = '?';
  ptr = strstr(firmware, "NDF:");
  if(NULL != ptr)
    {
      if((strlen(firmware) - strlen(ptr)) >= 7)
        {
          if(':' == *(ptr+5) && '*' == *(ptr+6))
            *firmwaretag = *(ptr+4);
        }
    }

  if('A' != *firmwaretag)
    {
      msyslog(LOG_CRIT, "NeoClock4X(%d): firmware version \"%c\" not supported with this driver version!", unit, *firmwaretag);
      return (0);
    }

  return (1);
}
#endif

#else
int refclock_neoclock4x_bs;
#endif /* REFCLOCK */

/*
 * History:
 * refclock_neoclock4x.c
 *
 * 2002/04/27 cjh
 * Revision 1.0  first release
 *
 * 2002/07/15 cjh
 * preparing for bitkeeper reposity
 *
 * 2002/09/09 cjh
 * Revision 1.1
 * - don't assume sprintf returns an int anymore
 * - change the way the firmware version is read
 * - some customers would like to put a device called
 *   data diode to the NeoClock4X device to disable
 *   the write line. We need to now the firmware
 *   version even in this case. We made a compile time
 *   definition in this case. The code was previously
 *   only available on request.
 *
 * 2003/01/08 cjh
 * Revision 1.11
 * - changing xprinf to xnprinf to avoid buffer overflows
 * - change some logic
 * - fixed memory leaks if drivers can't initialize
 *
 * 2003/01/10 cjh
 * Revision 1.12
 * - replaced ldiv
 * - add code to support FreeBSD
 *
 * 2003/07/07 cjh
 * Revision 1.13
 * - fix reporting of clock status
 *   changes. previously a bad clock
 *   status was never reset.
 *
 * 2004/04/07 cjh
 * Revision 1.14
 * - open serial port in a way
 *   AIX and some other OS can
 *   handle much better
 *
 * 2006/01/11 cjh
 * Revision 1.15
 * - remove some unsued #ifdefs
 * - fix nsec calculation, closes #499
 *
 */