Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1277 lines (1129 sloc) 31.5 KB
/*
* refclock_nmea.c - clock driver for an NMEA GPS CLOCK
* Michael Petry Jun 20, 1994
* based on refclock_heathn.c
*
* Updated to add support for Accord GPS Clock
* Venu Gopal Dec 05, 2007
* neo.venu@gmail.com, venugopal_d@pgad.gov.in
*
* Updated to process 'time1' fudge factor
* Venu Gopal May 05, 2008
*
* Converted to common PPSAPI code, separate PPS fudge time1
* from serial timecode fudge time2.
* Dave Hart July 1, 2009
* hart@ntp.org, davehart@davehart.com
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#if defined(REFCLOCK) && defined(CLOCK_NMEA)
#include <sys/stat.h>
#include <stdio.h>
#include <ctype.h>
#include "ntpd.h"
#include "ntp_io.h"
#include "ntp_unixtime.h"
#include "ntp_refclock.h"
#include "ntp_stdlib.h"
#include "ntp_calendar.h"
#ifdef HAVE_PPSAPI
# include "ppsapi_timepps.h"
# include "refclock_atom.h"
#endif /* HAVE_PPSAPI */
#ifdef SYS_WINNT
#undef write /* ports/winnt/include/config.h: #define write _write */
extern int async_write(int, const void *, unsigned int);
#define write(fd, data, octets) async_write(fd, data, octets)
#endif
#ifndef TIMESPECTOTS
#define TIMESPECTOTS(ptspec, pts) \
do { \
DTOLFP((ptspec)->tv_nsec * 1.0e-9, pts); \
(pts)->l_ui += (u_int32)((ptspec)->tv_sec) + JAN_1970; \
} while (0)
#endif
/*
* This driver supports NMEA-compatible GPS receivers
*
* Prototype was refclock_trak.c, Thanks a lot.
*
* The receiver used spits out the NMEA sentences for boat navigation.
* And you thought it was an information superhighway. Try a raging river
* filled with rapids and whirlpools that rip away your data and warp time.
*
* If HAVE_PPSAPI is defined code to use the PPSAPI will be compiled in.
* On startup if initialization of the PPSAPI fails, it will fall back
* to the "normal" timestamps.
*
* The PPSAPI part of the driver understands fudge flag2 and flag3. If
* flag2 is set, it will use the clear edge of the pulse. If flag3 is
* set, kernel hardpps is enabled.
*
* GPS sentences other than RMC (the default) may be enabled by setting
* the relevent bits of 'mode' in the server configuration line
* server 127.127.20.x mode X
*
* bit 0 - enables RMC (1)
* bit 1 - enables GGA (2)
* bit 2 - enables GLL (4)
* bit 3 - enables ZDA (8) - Standard Time & Date
* bit 3 - enables ZDG (8) - Accord GPS Clock's custom sentence with GPS time
* very close to standard ZDA
*
* Multiple sentences may be selected except when ZDG/ZDA is selected.
*
* bit 4/5/6 - selects the baudrate for serial port :
* 0 for 4800 (default)
* 1 for 9600
* 2 for 19200
* 3 for 38400
* 4 for 57600
* 5 for 115200
*/
#define NMEA_MESSAGE_MASK_OLD 0x07
#define NMEA_MESSAGE_MASK_SINGLE 0x08
#define NMEA_MESSAGE_MASK (NMEA_MESSAGE_MASK_OLD | NMEA_MESSAGE_MASK_SINGLE)
#define NMEA_BAUDRATE_MASK 0x70
#define NMEA_BAUDRATE_SHIFT 4
/*
* Definitions
*/
#define DEVICE "/dev/gps%d" /* GPS serial device */
#define PPSDEV "/dev/gpspps%d" /* PPSAPI device override */
#define SPEED232 B4800 /* uart speed (4800 bps) */
#define PRECISION (-9) /* precision assumed (about 2 ms) */
#define PPS_PRECISION (-20) /* precision assumed (about 1 us) */
#define REFID "GPS\0" /* reference id */
#define DESCRIPTION "NMEA GPS Clock" /* who we are */
#ifndef O_NOCTTY
#define M_NOCTTY 0
#else
#define M_NOCTTY O_NOCTTY
#endif
#ifndef O_NONBLOCK
#define M_NONBLOCK 0
#else
#define M_NONBLOCK O_NONBLOCK
#endif
#define PPSOPENMODE (O_RDWR | M_NOCTTY | M_NONBLOCK)
/* NMEA sentence array indexes for those we use */
#define NMEA_GPRMC 0 /* recommended min. nav. */
#define NMEA_GPGGA 1 /* fix and quality */
#define NMEA_GPGLL 2 /* geo. lat/long */
#define NMEA_GPZDA 3 /* date/time */
/*
* $GPZDG is a proprietary sentence that violates the spec, by not
* using $P and an assigned company identifier to prefix the sentence
* identifier. When used with this driver, the system needs to be
* isolated from other NTP networks, as it operates in GPS time, not
* UTC as is much more common. GPS time is >15 seconds different from
* UTC due to not respecting leap seconds since 1970 or so. Other
* than the different timebase, $GPZDG is similar to $GPZDA.
*/
#define NMEA_GPZDG 4
#define NMEA_ARRAY_SIZE (NMEA_GPZDG + 1)
/*
* Sentence selection mode bits
*/
#define USE_ALL 0 /* any/all */
#define USE_GPRMC 1
#define USE_GPGGA 2
#define USE_GPGLL 4
#define USE_GPZDA_ZDG 8 /* affects both */
/* mapping from sentence index to controlling mode bit */
u_char sentence_mode[NMEA_ARRAY_SIZE] =
{
USE_GPRMC,
USE_GPGGA,
USE_GPGLL,
USE_GPZDA_ZDG,
USE_GPZDA_ZDG
};
/*
* Unit control structure
*/
struct nmeaunit {
#ifdef HAVE_PPSAPI
struct refclock_atom atom; /* PPSAPI structure */
int ppsapi_tried; /* attempt PPSAPI once */
int ppsapi_lit; /* time_pps_create() worked */
int ppsapi_fd; /* fd used with PPSAPI */
int ppsapi_gate; /* allow edge detection processing */
int tcount; /* timecode sample counter */
int pcount; /* PPS sample counter */
#endif /* HAVE_PPSAPI */
l_fp tstamp; /* timestamp of last poll */
int gps_time; /* 0 UTC, 1 GPS time */
/* per sentence checksum seen flag */
struct calendar used; /* hh:mm:ss of used sentence */
u_char cksum_seen[NMEA_ARRAY_SIZE];
};
/*
* Function prototypes
*/
static int nmea_start (int, struct peer *);
static void nmea_shutdown (int, struct peer *);
static void nmea_receive (struct recvbuf *);
static void nmea_poll (int, struct peer *);
#ifdef HAVE_PPSAPI
static void nmea_control (int, struct refclockstat *,
struct refclockstat *, struct peer *);
static void nmea_timer (int, struct peer *);
#define NMEA_CONTROL nmea_control
#define NMEA_TIMER nmea_timer
#else
#define NMEA_CONTROL noentry
#define NMEA_TIMER noentry
#endif /* HAVE_PPSAPI */
static void gps_send (int, const char *, struct peer *);
static char * field_parse (char *, int);
static int nmea_checksum_ok(const char *);
static void nmea_day_unfold(struct calendar*);
static void nmea_century_unfold(struct calendar*);
/*
* Transfer vector
*/
struct refclock refclock_nmea = {
nmea_start, /* start up driver */
nmea_shutdown, /* shut down driver */
nmea_poll, /* transmit poll message */
NMEA_CONTROL, /* fudge control */
noentry, /* initialize driver */
noentry, /* buginfo */
NMEA_TIMER /* called once per second */
};
/*
* nmea_start - open the GPS devices and initialize data for processing
*/
static int
nmea_start(
int unit,
struct peer *peer
)
{
register struct nmeaunit *up;
struct refclockproc *pp;
int fd;
char device[20];
int baudrate;
char *baudtext;
pp = peer->procptr;
/*
* Open serial port. Use CLK line discipline, if available.
*/
snprintf(device, sizeof(device), DEVICE, unit);
/*
* Opening the serial port with appropriate baudrate
* based on the value of bit 4/5/6
*/
switch ((peer->ttl & NMEA_BAUDRATE_MASK) >> NMEA_BAUDRATE_SHIFT) {
case 0:
case 6:
case 7:
default:
baudrate = SPEED232;
baudtext = "4800";
break;
case 1:
baudrate = B9600;
baudtext = "9600";
break;
case 2:
baudrate = B19200;
baudtext = "19200";
break;
case 3:
baudrate = B38400;
baudtext = "38400";
break;
#ifdef B57600
case 4:
baudrate = B57600;
baudtext = "57600";
break;
#endif
#ifdef B115200
case 5:
baudrate = B115200;
baudtext = "115200";
break;
#endif
}
fd = refclock_open(device, baudrate, LDISC_CLK);
if (fd <= 0) {
#ifdef HAVE_READLINK
/* nmead support added by Jon Miner (cp_n18@yahoo.com)
*
* See http://home.hiwaay.net/~taylorc/gps/nmea-server/
* for information about nmead
*
* To use this, you need to create a link from /dev/gpsX
* to the server:port where nmead is running. Something
* like this:
*
* ln -s server:port /dev/gps1
*/
char buffer[80];
char *nmea_host, *nmea_tail;
int nmea_port;
int len;
struct hostent *he;
struct protoent *p;
struct sockaddr_in so_addr;
if ((len = readlink(device,buffer,sizeof(buffer))) == -1)
return(0);
buffer[len] = 0;
if ((nmea_host = strtok(buffer,":")) == NULL)
return(0);
if ((nmea_tail = strtok(NULL,":")) == NULL)
return(0);
nmea_port = atoi(nmea_tail);
if ((he = gethostbyname(nmea_host)) == NULL)
return(0);
if ((p = getprotobyname("tcp")) == NULL)
return(0);
memset(&so_addr, 0, sizeof(so_addr));
so_addr.sin_family = AF_INET;
so_addr.sin_port = htons(nmea_port);
so_addr.sin_addr = *((struct in_addr *) he->h_addr);
if ((fd = socket(PF_INET,SOCK_STREAM,p->p_proto)) == -1)
return(0);
if (connect(fd,(struct sockaddr *)&so_addr, sizeof(so_addr)) == -1) {
close(fd);
return (0);
}
#else
pp->io.fd = -1;
return (0);
#endif
}
msyslog(LOG_NOTICE, "%s serial %s open at %s bps",
refnumtoa(&peer->srcadr), device, baudtext);
/*
* Allocate and initialize unit structure
*/
up = emalloc(sizeof(*up));
memset(up, 0, sizeof(*up));
pp->io.clock_recv = nmea_receive;
pp->io.srcclock = (caddr_t)peer;
pp->io.datalen = 0;
pp->io.fd = fd;
if (!io_addclock(&pp->io)) {
pp->io.fd = -1;
close(fd);
free(up);
return (0);
}
pp->unitptr = (caddr_t)up;
/*
* Initialize miscellaneous variables
*/
peer->precision = PRECISION;
pp->clockdesc = DESCRIPTION;
memcpy(&pp->refid, REFID, 4);
gps_send(fd,"$PMOTG,RMC,0000*1D\r\n", peer);
return (1);
}
/*
* nmea_shutdown - shut down a GPS clock
*
* NOTE this routine is called after nmea_start() returns failure,
* as well as during a normal shutdown due to ntpq :config unpeer.
*/
static void
nmea_shutdown(
int unit,
struct peer *peer
)
{
register struct nmeaunit *up;
struct refclockproc *pp;
UNUSED_ARG(unit);
pp = peer->procptr;
up = (struct nmeaunit *)pp->unitptr;
if (up != NULL) {
#ifdef HAVE_PPSAPI
if (up->ppsapi_lit) {
time_pps_destroy(up->atom.handle);
if (up->ppsapi_fd != pp->io.fd)
close(up->ppsapi_fd);
}
#endif
free(up);
}
if (-1 != pp->io.fd)
io_closeclock(&pp->io);
}
/*
* nmea_control - configure fudge params
*/
#ifdef HAVE_PPSAPI
static void
nmea_control(
int unit,
struct refclockstat *in_st,
struct refclockstat *out_st,
struct peer *peer
)
{
char device[32];
register struct nmeaunit *up;
struct refclockproc *pp;
int pps_fd;
UNUSED_ARG(in_st);
UNUSED_ARG(out_st);
pp = peer->procptr;
up = (struct nmeaunit *)pp->unitptr;
if (!(CLK_FLAG1 & pp->sloppyclockflag)) {
if (!up->ppsapi_tried)
return;
up->ppsapi_tried = 0;
if (!up->ppsapi_lit)
return;
peer->flags &= ~FLAG_PPS;
peer->precision = PRECISION;
time_pps_destroy(up->atom.handle);
if (up->ppsapi_fd != pp->io.fd)
close(up->ppsapi_fd);
up->atom.handle = 0;
up->ppsapi_lit = 0;
up->ppsapi_fd = -1;
return;
}
if (up->ppsapi_tried)
return;
/*
* Light up the PPSAPI interface.
*/
up->ppsapi_tried = 1;
/*
* if /dev/gpspps$UNIT can be opened that will be used for
* PPSAPI. Otherwise, the GPS serial device /dev/gps$UNIT
* already opened is used for PPSAPI as well.
*/
snprintf(device, sizeof(device), PPSDEV, unit);
pps_fd = open(device, PPSOPENMODE, S_IRUSR | S_IWUSR);
if (-1 == pps_fd)
pps_fd = pp->io.fd;
if (refclock_ppsapi(pps_fd, &up->atom)) {
up->ppsapi_lit = 1;
up->ppsapi_fd = pps_fd;
/* prepare to use the PPS API for our own purposes now. */
refclock_params(pp->sloppyclockflag, &up->atom);
return;
}
NLOG(NLOG_CLOCKINFO)
msyslog(LOG_WARNING, "%s flag1 1 but PPSAPI fails",
refnumtoa(&peer->srcadr));
}
#endif /* HAVE_PPSAPI */
/*
* nmea_timer - called once per second, fetches PPS
* timestamp and stuffs in median filter.
*/
#ifdef HAVE_PPSAPI
static void
nmea_timer(
int unit,
struct peer * peer
)
{
struct nmeaunit *up;
struct refclockproc *pp;
UNUSED_ARG(unit);
pp = peer->procptr;
up = (struct nmeaunit *)pp->unitptr;
if (up->ppsapi_lit && up->ppsapi_gate &&
refclock_pps(peer, &up->atom, pp->sloppyclockflag) > 0) {
up->pcount++,
peer->flags |= FLAG_PPS;
peer->precision = PPS_PRECISION;
}
}
#endif /* HAVE_PPSAPI */
#ifdef HAVE_PPSAPI
/*
* This function is used to correlate a receive time stamp and a
* reference time with a PPS edge time stamp. It applies the necessary
* fudges (fudge1 for PPS, fudge2 for receive time) and then tries to
* move the receive time stamp to the corresponding edge. This can
* warp into future, if a transmission delay of more than 500ms is not
* compensated with a corresponding fudge time2 value, because then
* the next PPS edge is nearer than the last. (Similiar to what the
* PPS ATOM driver does, but we deal with full time stamps here, not
* just phase shift information.) Likewise, a negative fudge time2
* value must be used if the reference time stamp correlates with the
* *following* PPS pulse.
*
* Note that the receive time fudge value only needs to move the receive
* stamp near a PPS edge but that close proximity is not required;
* +/-100ms precision should be enough. But since the fudge value will
* probably also be used to compensate the transmission delay when no PPS
* edge can be related to the time stamp, it's best to get it as close
* as possible.
*
* It should also be noted that the typical use case is matching to
* the preceeding edge, as most units relate their sentences to the
* current second.
*
* The function returns PPS_RELATE_NONE (0) if no PPS edge correlation
* can be fixed; PPS_RELATE_EDGE (1) when a PPS edge could be fixed, but
* the distance to the reference time stamp is too big (exceeds +/-400ms)
* and the ATOM driver PLL cannot be used to fix the phase; and
* PPS_RELATE_PHASE (2) when the ATOM driver PLL code can be used.
*
* On output, the receive time stamp is replaced with the
* corresponding PPS edge time if a fix could be made; the PPS fudge
* is updated to reflect the proper fudge time to apply. (This implies
* that 'refclock_process_f()' must be used!)
*/
#define PPS_RELATE_NONE 0 /* no pps correlation possible */
#define PPS_RELATE_EDGE 1 /* recv time fixed, no phase lock */
#define PPS_RELATE_PHASE 2 /* recv time fixed, phase lock ok */
static int
refclock_ppsrelate(
const struct refclockproc *pp , /* for sanity */
const struct refclock_atom *ap , /* for PPS io */
const l_fp *reftime ,
l_fp *rd_stamp, /* i/o read stamp */
double pp_fudge, /* pps fudge */
double *rd_fudge) /* i/o read fudge */
{
pps_info_t pps_info;
struct timespec timeout;
l_fp pp_stamp, pp_delta;
double delta, idelta;
if (pp->leap == LEAP_NOTINSYNC)
return PPS_RELATE_NONE; /* clock is insane, no chance */
memset(&timeout, 0, sizeof(timeout));
memset(&pps_info, 0, sizeof(pps_info_t));
if (time_pps_fetch(ap->handle, PPS_TSFMT_TSPEC,
&pps_info, &timeout) < 0)
return PPS_RELATE_NONE;
/* get last active PPS edge before receive */
if (ap->pps_params.mode & PPS_CAPTUREASSERT)
timeout = pps_info.assert_timestamp;
else if (ap->pps_params.mode & PPS_CAPTURECLEAR)
timeout = pps_info.clear_timestamp;
else
return PPS_RELATE_NONE;
/* get delta between receive time and PPS time */
TIMESPECTOTS(&timeout, &pp_stamp);
pp_delta = *rd_stamp;
L_SUB(&pp_delta, &pp_stamp);
LFPTOD(&pp_delta, delta);
delta += pp_fudge - *rd_fudge;
if (fabs(delta) > 1.5)
return PPS_RELATE_NONE; /* PPS timeout control */
/* eventually warp edges, check phase */
idelta = floor(delta + 0.5);
pp_fudge -= idelta;
delta -= idelta;
if (fabs(delta) > 0.45)
return PPS_RELATE_NONE; /* dead band control */
/* we actually have a PPS edge to relate with! */
*rd_stamp = pp_stamp;
*rd_fudge = pp_fudge;
/* if whole system out-of-sync, do not try to PLL */
if (sys_leap == LEAP_NOTINSYNC)
return PPS_RELATE_EDGE; /* cannot PLL with atom code */
/* check against reftime if ATOM PLL can be used */
pp_delta = *reftime;
L_SUB(&pp_delta, &pp_stamp);
LFPTOD(&pp_delta, delta);
delta += pp_fudge;
if (fabs(delta) > 0.45)
return PPS_RELATE_EDGE; /* cannot PLL with atom code */
/* all checks passed, gets an AAA rating here! */
return PPS_RELATE_PHASE; /* can PLL with atom code */
}
#endif /* HAVE_PPSAPI */
/*
* nmea_receive - receive data from the serial interface
*/
static void
nmea_receive(
struct recvbuf *rbufp
)
{
register struct nmeaunit *up;
struct refclockproc *pp;
struct peer *peer;
char *cp, *dp, *msg;
u_char sentence;
/* Use these variables to hold data until we decide its worth
* keeping */
char rd_lastcode[BMAX];
l_fp rd_timestamp, reftime;
int rd_lencode;
double rd_fudge;
struct calendar date;
/*
* Initialize pointers and read the timecode and timestamp
*/
peer = rbufp->recv_peer;
pp = peer->procptr;
up = (struct nmeaunit *)pp->unitptr;
rd_lencode = refclock_gtlin(
rbufp,
rd_lastcode,
sizeof(rd_lastcode),
&rd_timestamp);
/*
* There is a case that a <CR><LF> gives back a "blank" line.
* We can't have a well-formed sentence with less than 8 chars.
*/
if (0 == rd_lencode)
return;
if (rd_lencode < 8) {
refclock_report(peer, CEVNT_BADREPLY);
return;
}
DPRINTF(1, ("nmea: gpsread %d %s\n", rd_lencode, rd_lastcode));
/*
* We check the timecode format and decode its contents. The
* we only care about a few of them. The most important being
* the $GPRMC format
* $GPRMC,hhmmss,a,fddmm.xx,n,dddmmm.xx,w,zz.z,yyy.,ddmmyy,dd,v*CC
* mode (0,1,2,3) selects sentence ANY/ALL, RMC, GGA, GLL, ZDA
* $GPGLL,3513.8385,S,14900.7851,E,232420.594,A*21
* $GPGGA,232420.59,3513.8385,S,14900.7851,E,1,05,3.4,00519,M,,,,*3F
* $GPRMC,232418.19,A,3513.8386,S,14900.7853,E,00.0,000.0,121199,12.,E*77
*
* Defining GPZDA to support Standard Time & Date
* sentence. The sentence has the following format
*
* $--ZDA,HHMMSS.SS,DD,MM,YYYY,TH,TM,*CS<CR><LF>
*
* Apart from the familiar fields,
* 'TH' Time zone Hours
* 'TM' Time zone Minutes
*
* Defining GPZDG to support Accord GPS Clock's custom NMEA
* sentence. The sentence has the following format
*
* $GPZDG,HHMMSS.S,DD,MM,YYYY,AA.BB,V*CS<CR><LF>
*
* It contains the GPS timestamp valid for next PPS pulse.
* Apart from the familiar fields,
* 'AA.BB' denotes the signal strength( should be < 05.00 )
* 'V' denotes the GPS sync status :
* '0' indicates INVALID time,
* '1' indicates accuracy of +/-20 ms
* '2' indicates accuracy of +/-100 ns
*/
cp = rd_lastcode;
if (cp[0] == '$') {
/* Allow for GLGGA and GPGGA etc. */
msg = cp + 3;
if (strncmp(msg, "RMC", 3) == 0)
sentence = NMEA_GPRMC;
else if (strncmp(msg, "GGA", 3) == 0)
sentence = NMEA_GPGGA;
else if (strncmp(msg, "GLL", 3) == 0)
sentence = NMEA_GPGLL;
else if (strncmp(msg, "ZDG", 3) == 0)
sentence = NMEA_GPZDG;
else if (strncmp(msg, "ZDA", 3) == 0)
sentence = NMEA_GPZDA;
else
return;
} else
return;
/* See if I want to process this message type */
if ((peer->ttl & NMEA_MESSAGE_MASK) &&
!(peer->ttl & sentence_mode[sentence]))
return;
/*
* $GPZDG provides GPS time not UTC, and the two mix poorly.
* Once have processed a $GPZDG, do not process any further
* UTC sentences (all but $GPZDG currently).
*/
if (up->gps_time && NMEA_GPZDG != sentence)
return;
/*
* Apparently, older NMEA specifications (which are expensive)
* did not require the checksum for all sentences. $GPMRC is
* the only one so far identified which has always been required
* to include a checksum.
*
* Today, most NMEA GPS receivers checksum every sentence. To
* preserve its error-detection capabilities with modern GPSes
* while allowing operation without checksums on all but $GPMRC,
* we keep track of whether we've ever seen a checksum on a
* given sentence, and if so, reject future checksum failures.
*/
if (nmea_checksum_ok(rd_lastcode)) {
up->cksum_seen[sentence] = TRUE;
} else if (NMEA_GPRMC == sentence || up->cksum_seen[sentence]) {
refclock_report(peer, CEVNT_BADREPLY);
return;
}
cp = rd_lastcode;
/* Grab field depending on clock string type */
memset(&date, 0, sizeof(date));
switch (sentence) {
case NMEA_GPRMC:
/*
* Test for synchronization. Check for quality byte.
*/
dp = field_parse(cp, 2);
if (dp[0] != 'A')
pp->leap = LEAP_NOTINSYNC;
else
pp->leap = LEAP_NOWARNING;
/* Now point at the time field */
dp = field_parse(cp, 1);
break;
case NMEA_GPGGA:
/*
* Test for synchronization. Check for quality byte.
*/
dp = field_parse(cp, 6);
if (dp[0] == '0')
pp->leap = LEAP_NOTINSYNC;
else
pp->leap = LEAP_NOWARNING;
/* Now point at the time field */
dp = field_parse(cp, 1);
break;
case NMEA_GPGLL:
/*
* Test for synchronization. Check for quality byte.
*/
dp = field_parse(cp, 6);
if (dp[0] != 'A')
pp->leap = LEAP_NOTINSYNC;
else
pp->leap = LEAP_NOWARNING;
/* Now point at the time field */
dp = field_parse(cp, 5);
break;
case NMEA_GPZDG:
/* For $GPZDG check for validity of GPS time. */
dp = field_parse(cp, 6);
if (dp[0] == '0')
pp->leap = LEAP_NOTINSYNC;
else
pp->leap = LEAP_NOWARNING;
/* fall through to NMEA_GPZDA */
case NMEA_GPZDA:
if (NMEA_GPZDA == sentence)
pp->leap = LEAP_NOWARNING;
/* Now point at the time field */
dp = field_parse(cp, 1);
break;
default:
return;
}
/*
* Check time code format of NMEA
*/
if (!isdigit((int)dp[0]) ||
!isdigit((int)dp[1]) ||
!isdigit((int)dp[2]) ||
!isdigit((int)dp[3]) ||
!isdigit((int)dp[4]) ||
!isdigit((int)dp[5])) {
DPRINTF(1, ("NMEA time code %c%c%c%c%c%c non-numeric",
dp[0], dp[1], dp[2], dp[3], dp[4], dp[5]));
refclock_report(peer, CEVNT_BADTIME);
return;
}
/*
* Convert time and check values.
*/
date.hour = ((dp[0] - '0') * 10) + dp[1] - '0';
date.minute = ((dp[2] - '0') * 10) + dp[3] - '0';
date.second = ((dp[4] - '0') * 10) + dp[5] - '0';
/*
* Default to 0 milliseconds, if decimal convert milliseconds in
* one, two or three digits
*/
pp->nsec = 0;
if (dp[6] == '.') {
if (isdigit((int)dp[7])) {
pp->nsec = (dp[7] - '0') * 100000000;
if (isdigit((int)dp[8])) {
pp->nsec += (dp[8] - '0') * 10000000;
if (isdigit((int)dp[9])) {
pp->nsec += (dp[9] - '0') * 1000000;
}
}
}
}
if (date.hour > 23 || date.minute > 59 ||
date.second > 59 || pp->nsec > 1000000000) {
DPRINTF(1, ("NMEA hour/min/sec/nsec range %02d:%02d:%02d.%09ld\n",
pp->hour, pp->minute, pp->second, pp->nsec));
refclock_report(peer, CEVNT_BADTIME);
return;
}
/*
* Used only the first recognized sentence each second.
*/
if (date.hour == up->used.hour &&
date.minute == up->used.minute &&
date.second == up->used.second)
return;
pp->lencode = (u_short)rd_lencode;
memcpy(pp->a_lastcode, rd_lastcode, pp->lencode + 1);
up->tstamp = rd_timestamp;
pp->lastrec = up->tstamp;
DPRINTF(1, ("nmea: timecode %d %s\n", pp->lencode, pp->a_lastcode));
/*
* Convert date and check values.
*/
if (NMEA_GPRMC == sentence) {
dp = field_parse(cp,9);
date.monthday = 10 * (dp[0] - '0') + (dp[1] - '0');
date.month = 10 * (dp[2] - '0') + (dp[3] - '0');
date.year = 10 * (dp[4] - '0') + (dp[5] - '0');
nmea_century_unfold(&date);
} else if (NMEA_GPZDA == sentence || NMEA_GPZDG == sentence) {
dp = field_parse(cp, 2);
date.monthday = 10 * (dp[0] - '0') + (dp[1] - '0');
dp = field_parse(cp, 3);
date.month = 10 * (dp[0] - '0') + (dp[1] - '0');
dp = field_parse(cp, 4);
date.year = 1000 * (dp[0] - '0') + 100 * (dp[1] - '0')
+ 10 * (dp[2] - '0') + (dp[3] - '0');
} else
nmea_day_unfold(&date);
if (date.month < 1 || date.month > 12 ||
date.monthday < 1 || date.monthday > 31) {
refclock_report(peer, CEVNT_BADDATE);
return;
}
up->used.hour = date.hour;
up->used.minute = date.minute;
up->used.second = date.second;
/*
* If "fudge 127.127.20.__ flag4 1" is configured in ntp.conf,
* remove the location and checksum from the NMEA sentence
* recorded as the last timecode and visible to remote users
* with:
*
* ntpq -c clockvar <server>
*
* Note that this also removes the location from the clockstats
* log (if it is enabled). Some NTP operators monitor their
* NMEA GPS using the change in location in clockstats over
* time as as a proxy for the quality of GPS reception and
* thereby time reported.
*/
if (CLK_FLAG4 & pp->sloppyclockflag) {
/*
* Start by pointing cp and dp at the fields with
* longitude and latitude in the last timecode.
*/
switch (sentence) {
case NMEA_GPGLL:
cp = field_parse(pp->a_lastcode, 1);
dp = field_parse(cp, 2);
break;
case NMEA_GPGGA:
cp = field_parse(pp->a_lastcode, 2);
dp = field_parse(cp, 2);
break;
case NMEA_GPRMC:
cp = field_parse(pp->a_lastcode, 3);
dp = field_parse(cp, 2);
break;
case NMEA_GPZDA:
case NMEA_GPZDG:
default:
cp = dp = NULL;
}
/* Blank the entire latitude & longitude. */
while (cp) {
while (',' != *cp) {
if ('.' != *cp)
*cp = '_';
cp++;
}
/* Longitude at cp then latitude at dp */
if (cp < dp)
cp = dp;
else
cp = NULL;
}
/* Blank the checksum, the last two characters */
if (dp) {
cp = pp->a_lastcode + pp->lencode - 2;
if (0 == cp[2])
cp[0] = cp[1] = '_';
}
}
/*
* Get the reference time stamp from the calendar buffer.
* Process the new sample in the median filter and determine
* the timecode timestamp, but only if the PPS is not in
* control.
*/
rd_fudge = pp->fudgetime2;
date.yearday = 0; /* make sure it's not used */
DTOLFP(pp->nsec * 1.0e-9, &reftime);
reftime.l_ui += caltontp(&date);
/* $GPZDG postprocessing first... */
if (NMEA_GPZDG == sentence) {
/*
* Note if we're only using GPS timescale from now on.
*/
if (!up->gps_time) {
up->gps_time = 1;
NLOG(NLOG_CLOCKINFO)
msyslog(LOG_INFO, "%s using only $GPZDG",
refnumtoa(&peer->srcadr));
}
/*
* $GPZDG indicates the second after the *next* PPS
* pulse. So we remove 1 second from the reference
* time now.
*/
reftime.l_ui--;
}
#ifdef HAVE_PPSAPI
up->tcount++;
/*
* If we have PPS running, we try to associate the sentence with
* the last active edge of the PPS signal.
*/
if (up->ppsapi_lit)
switch (refclock_ppsrelate(pp, &up->atom, &reftime,
&rd_timestamp, pp->fudgetime1,
&rd_fudge))
{
case PPS_RELATE_EDGE:
up->ppsapi_gate = 0;
break;
case PPS_RELATE_PHASE:
up->ppsapi_gate = 1;
break;
default:
break;
}
else
up->ppsapi_gate = 0;
if (up->ppsapi_gate && (peer->flags & FLAG_PPS))
return;
#endif /* HAVE_PPSAPI */
refclock_process_offset(pp, reftime, rd_timestamp, rd_fudge);
}
/*
* nmea_poll - called by the transmit procedure
*
* We go to great pains to avoid changing state here, since there may be
* more than one eavesdropper receiving the same timecode.
*/
static void
nmea_poll(
int unit,
struct peer *peer
)
{
register struct nmeaunit *up;
struct refclockproc *pp;
pp = peer->procptr;
up = (struct nmeaunit *)pp->unitptr;
/*
* Process median filter samples. If none received, declare a
* timeout and keep going.
*/
#ifdef HAVE_PPSAPI
if (up->pcount == 0) {
peer->flags &= ~FLAG_PPS;
peer->precision = PRECISION;
}
if (up->tcount == 0) {
pp->coderecv = pp->codeproc;
refclock_report(peer, CEVNT_TIMEOUT);
return;
}
up->pcount = up->tcount = 0;
#else /* HAVE_PPSAPI */
if (pp->coderecv == pp->codeproc) {
refclock_report(peer, CEVNT_TIMEOUT);
return;
}
#endif /* HAVE_PPSAPI */
pp->polls++;
pp->lastref = pp->lastrec;
refclock_receive(peer);
record_clock_stats(&peer->srcadr, pp->a_lastcode);
/*
* usually nmea_receive can get a timestamp every second,
* but at least one Motorola unit needs prompting each
* time.
*/
gps_send(pp->io.fd,"$PMOTG,RMC,0000*1D\r\n", peer);
}
/*
*
* gps_send(fd,cmd, peer) Sends a command to the GPS receiver.
* as gps_send(fd,"rqts,u\r", peer);
*
* We don't currently send any data, but would like to send
* RTCM SC104 messages for differential positioning. It should
* also give us better time. Without a PPS output, we're
* Just fooling ourselves because of the serial code paths
*
*/
static void
gps_send(
int fd,
const char *cmd,
struct peer *peer
)
{
if (write(fd, cmd, strlen(cmd)) == -1) {
refclock_report(peer, CEVNT_FAULT);
}
}
static char *
field_parse(
char *cp,
int fn
)
{
char *tp;
int i = fn;
for (tp = cp; i && *tp; tp++)
if (*tp == ',')
i--;
return tp;
}
/*
* nmea_checksum_ok verifies 8-bit XOR checksum is correct then returns 1
*
* format is $XXXXX,1,2,3,4*ML
*
* 8-bit XOR of characters between $ and * noninclusive is transmitted
* in last two chars M and L holding most and least significant nibbles
* in hex representation such as:
*
* $GPGLL,5057.970,N,00146.110,E,142451,A*27
* $GPVTG,089.0,T,,,15.2,N,,*7F
*/
int
nmea_checksum_ok(
const char *sentence
)
{
u_char my_cs;
u_long input_cs;
const char *p;
my_cs = 0;
p = sentence;
if ('$' != *p++)
return 0;
for ( ; *p && '*' != *p; p++) {
my_cs ^= *p;
}
if ('*' != *p++)
return 0;
if (0 == p[0] || 0 == p[1] || 0 != p[2])
return 0;
if (0 == hextoint(p, &input_cs))
return 0;
if (my_cs != input_cs)
return 0;
return 1;
}
/*
* -------------------------------------------------------------------
* funny calendar-oriented stuff -- a bit hard to grok.
* -------------------------------------------------------------------
*/
/*
* Do a periodic unfolding of a truncated value around a given pivot
* value.
* The result r will hold to pivot <= r < pivot+period (period>0) or
* pivot+period < r <= pivot (period < 0) and value % period == r % period,
* using floor division convention.
*/
static time_t
nmea_periodic_unfold(
time_t pivot,
time_t value,
time_t period)
{
/*
* This will only work as long as 'value - pivot%period' does
* not create a signed overflow condition.
*/
value = (value - (pivot % period)) % period;
if (value && (value ^ period) < 0)
value += period;
return pivot + value;
}
/*
* Unfold a time-of-day (seconds since midnight) around the current
* system time in a manner that guarantees an absolute difference of
* less than 12hrs.
*
* This function is used for NMEA sentences that contain no date
* information. This requires the system clock to be in +/-12hrs
* around the true time, or the clock will synchronize the system 1day
* off if not augmented with a time sources that also provide the
* necessary date information.
*
* The function updates the refclockproc structure is also uses as
* input to fetch the time from.
*/
static void
nmea_day_unfold(
struct calendar *jd)
{
time_t value, pivot;
struct tm *tdate;
value = ((time_t)jd->hour * MINSPERHR
+ (time_t)jd->minute) * SECSPERMIN
+ (time_t)jd->second;
pivot = time(NULL) - SECSPERDAY/2;
value = nmea_periodic_unfold(pivot, value, SECSPERDAY);
tdate = gmtime(&value);
if (tdate) {
jd->year = tdate->tm_year + 1900;
jd->yearday = tdate->tm_yday + 1;
jd->month = tdate->tm_mon + 1;
jd->monthday = tdate->tm_mday;
jd->hour = tdate->tm_hour;
jd->minute = tdate->tm_min;
jd->second = tdate->tm_sec;
} else {
jd->year = 0;
jd->yearday = 0;
jd->month = 0;
jd->monthday = 0;
}
}
/*
* Unfold a 2-digit year into full year spec around the current year
* of the system time. This requires the system clock to be in -79/+19
* years around the true time, or the result will be off by
* 100years. The assymetric behaviour was chosen to enable inital sync
* for systems that do not have a battery-backup-clock and start with
* a date that is typically years in the past.
*
* The function updates the calendar structure that is also used as
* input to fetch the year from.
*/
static void
nmea_century_unfold(
struct calendar *jd)
{
time_t pivot_time;
struct tm *pivot_date;
time_t pivot_year;
/* get warp limit and century start of pivot from system time */
pivot_time = time(NULL);
pivot_date = gmtime(&pivot_time);
pivot_year = pivot_date->tm_year + 1900 - 20;
jd->year = nmea_periodic_unfold(pivot_year, jd->year, 100);
}
#else
int refclock_nmea_bs;
#endif /* REFCLOCK && CLOCK_NMEA */