Address: 127.127.20.u
- Reference ID: GPS
- Driver ID: GPS_NMEA
- Serial Port: /dev/gpsu; 4800 - 115200 bps, 8-bits, no parity
- Serial Port: /dev/gpsppsu; for just the PPS signal (this is tried first for PPS, before /dev/gpsu)
- Serial Port: /dev/gpsu; symlink to server:port (for nmead) Features: tty_clk
-
Description
-
This driver supports GPS receivers with the $GPRMC, $GPGLL, $GPGGA, $GPZDA, and $GPZDG NMEA sentences by default. Note that Accord's custom NMEA sentence $GPZDG reports using the GPS timescale, while the rest of the sentences report UTC. The difference between the two is a whole number of seconds which increases with each leap second insertion in UTC. To avoid problems mixing UTC and GPS timescales, the driver disables processing of UTC sentences once $GPZDG is received.
-
The driver expects the receiver to be set up to transmit at least one supported sentence every second.
-
The accuracy depends on the receiver used. Inexpensive GPS models are available with a claimed PPS signal accuracy of 1 ms or better relative to the broadcast signal. However, in most cases the actual accuracy is limited by the precision of the timecode and the latencies of the serial interface and operating system.
-
If the Operating System supports PPSAPI (RFC 2783), fudge flag1 1 enables its use.
-
The various GPS sentences that this driver recognises look like this:
- (others quietly ignored)
-
$GPRMC,UTC,POS_STAT,LAT,LAT_REF,LON,LON_REF,SPD,HDG,DATE,MAG_VAR,MAG_REF*CS<cr><lf>
-$GPGLL,LAT,LAT_REF,LONG,LONG_REF,UTC,POS_STAT*CS<cr><lf>
-$GPGGA,UTC,LAT,LAT_REF,LONG,LONG_REF,FIX_MODE,SAT_USED,HDOP,ALT,ALT_UNIT,GEO,G_UNIT,D_AGE,D_REF*CS<cr><lf>
-$GPZDA,UTC,DD,MM,YYYY,TH,TM,*CS<cr><lf>
-$GPZDG,GPSTIME,DD,MM,YYYY,AA.BB,V*CS<cr><lf>
-
- UTC - Time of day on UTC timescale. Hours, minutes and seconds [fraction (opt.)]. (hhmmss[.fff])
- POS_STAT - Position status. (A = Data valid, V = Data invalid)
- LAT - Latitude (llll.ll)
- LAT_REF - Latitude direction. (N = North, S = South)
- LON - Longitude (yyyyy.yy)
- LON_REF - Longitude direction (E = East, W = West)
- SPD - Speed over ground. (knots) (x.x)
- HDG - Heading/track made good (degrees True) (x.x)
- DATE - Date (ddmmyy)
- MAG_VAR - Magnetic variation (degrees) (x.x)
- MAG_REF - Magnetic variation (E = East, W = West)
- FIX_MODE - Position Fix Mode (0 = Invalid, >0 = Valid)
- SAT_USED - Number Satellites used in solution
- HDOP - Horizontal Dilution of Precision
- ALT - Antenna Altitude
- ALT_UNIT - Altitude Units (Metres/Feet)
- GEO - Geoid/Elipsoid separation
- G_UNIT - Geoid units (M/F)
- D_AGE - Age of last DGPS Fix
- D_REF - Reference ID of DGPS station
- GPSTIME - Time of day on GPS timescale. Hours, minutes and seconds [fraction (opt.)]. (hhmmss[.f])
- DD - Day of the month (1-31)
- MM - Month of the year (1-12)
- YYYY - Year
- AA.BB - Denotes the signal strength (should be < 05.00)
- V - GPS sync status
- '0' => INVALID time,
- '1' => accuracy of +/- 20ms,
- '2' => accuracy of +/- 100ns
- CS - Checksum
- <cr><lf> - Sentence terminator.
-
-
Specific GPS sentences and bitrates may be selected by setting bits of the 'mode' in the server configuration line:
- server 127.127.20.x mode X bit 0 - process $GPMRC (value = 1) bit 1 - process $GPGGA (value = 2) bit 2 - process $GPGLL (value = 4) bit 4 - process $GPZDA or $GPZDG (value = 8)
-
The default (mode 0) is to process all supported sentences, which results in the last received each cycle being used. Multiple sentences may be selected by adding their mode bit values. The driver uses 4800 bits per second by default. Faster bitrates can be selected using bits 4, 5, and 6 of the mode field:
- bits 4/5/6 - select serial bitrate (0 for 4800 - the default, 16 for 9600, 32 for 19200, 48 for 38400, 64 for 57600, 80 for 115200)
-
The driver will send a $PMOTG,RMC,0000*1D<cr><lf> command each poll interval. This is not needed on most GPS receivers because they automatically send $GPRMC every second, but helps a Motorola GPS receiver that is otherwise silent. NMEA devices ignore commands they do not understand.
-
Setting up the Garmin GPS-25XL
- Switch off all output with by sending it the following string.
-
"$PGRMO,,2<cr><lf>"
-
Now switch only $GPRMC on by sending it the following string.
-
"$PGRMO,GPRMC,1<cr><lf>"
-
On some systems the PPS signal isn't switched on by default. It can be switched on by sending the following string.
-
"$PGRMC,,,,,,,,,,,,2<cr><lf>"
-
Monitor Data
-
The GPS sentence that is used is written to the clockstats file and available with ntpq -c clockvar.
-
Fudge Factors
-
-
time1 time
-
Specifies the PPS time offset calibration factor, in seconds and fraction, with default 0.0.
-
time2 time
-
Specifies the serial end of line time offset calibration factor, in seconds and fraction, with default 0.0.
-
stratum number
-
Specifies the driver stratum, in decimal from 0 to 15, with default 0.
-
refid string
-
Specifies the driver reference identifier, an ASCII string from one to four characters, with default GPS.
-
flag1 0 | 1
-
Disable PPS signal processing if 0 (default); enable PPS signal processing if 1.
-
flag2 0 | 1
-
If PPS signal processing is enabled, capture the pulse on the rising edge if 0 (default); capture on the falling edge if 1.
-
flag3 0 | 1
-
If PPS signal processing is enabled, use the ntpd clock discipline if 0 (default); use the kernel discipline if 1.
-
flag4 0 | 1
-
Obscures location in timecode: 0 for disable (default), 1 for enable.
-
+ Address: 127.127.20.u
+ Reference ID: GPS
+ Driver ID: GPS_NMEA
+ Serial Port: /dev/gpsu; 4800 - 115200 bps, 8-bits, no parity
+ Serial Port: /dev/gpsppsu; for just the PPS signal (this
+ is tried first for PPS, before /dev/gpsu)
+ Serial Port: /dev/gpsu; symlink to server:port (for nmead)
+ Features: tty_clk
+
+
+
Description
+
+
+ This driver supports GPS receivers with
+ the $GPRMC, $GPGLL, $GPGGA, $GPZDA
+ and $GPZDG NMEA sentences by default. Note that Accord's
+ custom NMEA sentence $GPZDG reports using the GPS timescale,
+ while the rest of the sentences report UTC. The difference between
+ the two is a whole number of seconds which increases with each leap
+ second insertion in UTC. To avoid problems mixing UTC and GPS
+ timescales, the driver disables processing of UTC sentences
+ once $GPZDG is received.
+
+
+ The driver expects the receiver to be set up to transmit at least one
+ supported sentence every second.
+
+
+ The accuracy depends on the receiver used. Inexpensive GPS models are
+ available with a claimed PPS signal accuracy of
+ 1 ms or better relative to the broadcast
+ signal. However, in most cases the actual accuracy is limited by the
+ precision of the timecode and the latencies of the serial interface and
+ operating system.
+
+
+ If the Operating System supports PPSAPI
+ (RFC 2783), fudge flag1
+ 1 enables its use.
+
+
+ The various GPS sentences that this driver recognises look like this:
+ (others quietly ignored)
+
Time of day on UTC timescale. Hours, minutes and seconds [fraction (opt.)]. (hhmmss[.fff])
+
+
POS_STAT
+
Position status. (A = Data valid, V = Data invalid)
+
+
LAT
+
Latitude (llll.ll)
+
+
LAT_REF
+
Latitude direction. (N = North, S = South)
+
+
LON
+
Longitude (yyyyy.yy)
+
+
LON_REF
+
Longitude direction (E = East, W = West)
+
+
SPD
+
Speed over ground. (knots) (x.x)
+
+
HDG
+
Heading/track made good (degrees True) (x.x)
+
+
DATE
+
Date (ddmmyy)
+
+
MAG_VAR
+
Magnetic variation (degrees) (x.x)
+
+
MAG_REF
+
Magnetic variation (E = East, W = West)
+
+
FIX_MODE
+
Position Fix Mode (0 = Invalid, >0 = Valid)
+
+
SAT_USED
+
Number of Satellites used in solution
+
+
HDOP
+
Horizontal Dilution of Precision
+
+
ALT
+
Antenna Altitude
+
+
ALT_UNIT
+
Altitude Units (Metres/Feet)
+
+
GEO
+
Geoid/Elipsoid separation
+
+
G_UNIT
+
Geoid units (M/F)
+
+
D_AGE
+
Age of last DGPS Fix
+
+
D_REF
+
Reference ID of DGPS station
+
+
GPSTIME
+
Time of day on GPS timescale. Hours, minutes and seconds [fraction (opt.)]. (hhmmss[.f])
+
+
DD
+
Day of the month (1-31)
+
+
MM
+
Month of the year (1-12)
+
+
YYYY
+
Year
+
+
AA.BB
+
Denotes the signal strength (should be < 05.00)
+
+
V
+
GPS sync status
+ '0' => INVALID time,
+ '1' => accuracy of +/- 20ms,
+ '2' => accuracy of +/- 100ns
+
+
CS
+
Checksum
+
+
<cr><lf>
+
Sentence terminator.
+
+
+
+
+
The 'mode' byte
+
+
+ Specific GPS sentences and bitrates may be selected by setting bits of
+ the 'mode' in the server configuration line: server
+ 127.127.20.x mode X
+
+
+
+
mode byte bits and bit groups
+
+
Bit
+
Dec.
+
Meaning
+
+
+
+
0
+
1
+
process $GPMRC
+
+
1
+
2
+
process $GPGGA
+
+
2
+
4
+
process $GPGLL
+
+
4
+
8
+
process $GPZDA or $GPZDG
+
+
4-6
+
0
+
linespeed 4800bps
+
+
16
+
linespeed 9600bps
+
+
32
+
linespeed 19200bps
+
+
48
+
linespeed 38400bps
+
+
64
+
linespeed 57600bps
+
+
80
+
linespeed 115200bps
+
+
7
+
128
+
Write the sub-second fraction of the receive time stamp to the
+ clockstat file for all recognised NMEA sentences. This can be used to
+ get a useful value for fudge time2. Caveat: This
+ will fill your clockstat file rather fast. Use it only temporarily to
+ get the numbers for the NMEA sentence of your choice.
+
+
+
+
+
+ The default (mode 0) is to process all supported sentences at a linespeed
+ of 4800bps, which results in the first one received and recognised in
+ each cycle being used. If only specific sentences should be
+ recognised, then the mode byte must be chosen to enable only the selected
+ ones. Multiple sentences may be selected by adding their mode bit
+ values, but of those enabled still only the first received sentence in a
+ cycle will be used. Using more than one sentence per cycle is
+ impossible, because
+
+
there is only fudge time2 available to
+ compensate for transmission delays but every sentence would need a
+ different one and
+
using more than one sentence per cycle overstuffs the internal data
+ filters.
+
+ The driver uses 4800 bits per second by default, but faster bitrates can
+ be selected using bits 4 to 6 of the mode field.
+
+
+
+ Caveat: Using higher line speeds does not necessarily
+ increase the precision of the timing device. Higher line speeds are
+ not necessarily helpful for the NMEA driver, either. They can be
+ used to accomodate for an amount of data that does not fit into a
+ 1-second cycle at 4800bps, but high-speed high-volume NMEA data is likely
+ to cause trouble with the serial line driver since NMEA supports no
+ protocol handshake. Any device that is exclusively used for time
+ synchronisation purposes should be configured to transmit the relevant
+ data only, e.g. one $GPRMC or $GPZDA per second, at a
+ linespeed of 4800bps or eventually 9600bps.
+
+
+
+ Configuring
+ NMEA Refclocks might give further useful hints for specific hardware
+ devices that exhibit strange or curious behaviour.
+
+
+
+ To make a specific setting, select the corresponding decimal values from
+ the mode byte table, add them all together and enter the resulting
+ decimal value into the clock configuration line.
+
+
+
+ The driver will send a $PMOTG,RMC,0000*1D<cr><lf>
+ command each poll interval. This is not needed on most GPS
+ receivers because they automatically send $GPRMC every second,
+ but helps a Motorola GPS receiver that is otherwise silent. NMEA
+ devices ignore commands they do not understand.
+
+
+
Setting up the Garmin GPS-25XL
+
+ Switch off all output with by sending it the following string.
+
"$PGRMO,,2<cr><lf>"
+
Now switch only $GPRMC on by sending it the following string.
+
"$PGRMO,GPRMC,1<cr><lf>"
+
+
On some systems the PPS signal isn't switched on by default. It can be
+ switched on by sending the following string.
+
"$PGRMC,,,,,,,,,,,,2<cr><lf>"
+
+
Monitor Data
+
+
The GPS sentence that is used is written to the clockstats file and
+ available with ntpq -c clockvar.
+
+
Fudge Factors
+
+
+
time1 time
+
Specifies the PPS time offset calibration factor, in seconds and fraction, with default 0.0.
+
+
+
+
diff --git a/ntpd/refclock_nmea.c b/ntpd/refclock_nmea.c
index e2552fc97..094c3fb00 100644
--- a/ntpd/refclock_nmea.c
+++ b/ntpd/refclock_nmea.c
@@ -32,6 +32,7 @@
#include "ntp_refclock.h"
#include "ntp_stdlib.h"
#include "ntp_calendar.h"
+#include "lib_strbuf.h"
#ifdef HAVE_PPSAPI
# include "ppsapi_timepps.h"
@@ -91,12 +92,49 @@ extern int async_write(int, const void *, unsigned int);
* 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_MESSAGE_MASK 0x0F
+#define NMEA_BAUDRATE_MASK 0x70
+#define NMEA_BAUDRATE_SHIFT 4
+#define NMEA_DELAYMEAS_MASK 0x80
-#define NMEA_BAUDRATE_MASK 0x70
-#define NMEA_BAUDRATE_SHIFT 4
+#define NMEA_PROTO_IDLEN 5 /* tag name must be at least 5 chars */
+#define NMEA_PROTO_MINLEN 6 /* min chars in sentence, excluding CS */
+#define NMEA_PROTO_MAXLEN 80 /* max chars in sentence, excluding CS */
+#define NMEA_PROTO_FIELDS 32 /* not official; limit on fields per record */
+
+/*
+ * We check the timecode format and decode its contents. 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
+ *
+ * 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
+ *
+ * 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
+ */
/*
* Definitions
@@ -144,18 +182,34 @@ extern int async_write(int, const void *, unsigned int);
#define USE_GPRMC 1
#define USE_GPGGA 2
#define USE_GPGLL 4
-#define USE_GPZDA_ZDG 8 /* affects both */
+#define USE_GPZDA 8
/* mapping from sentence index to controlling mode bit */
-u_char sentence_mode[NMEA_ARRAY_SIZE] =
+static const u_char sentence_mode[NMEA_ARRAY_SIZE] =
{
USE_GPRMC,
USE_GPGGA,
USE_GPGLL,
- USE_GPZDA_ZDG,
- USE_GPZDA_ZDG
+ USE_GPZDA,
+ USE_GPZDA
+};
+
+/* date formats we support */
+enum date_fmt {
+ DATE_1_DDMMYY, /* use 1 field with 2-digit year */
+ DATE_3_DDMMYYYY /* use 3 fields with 4-digit year */
};
+/* results for 'field_init()'
+ *
+ * Note: If a checksum is present, the checksum test must pass OK or the
+ * sentence is tagged invalid.
+ */
+#define CHECK_EMPTY -1 /* no data */
+#define CHECK_INVALID 0 /* not a valid NMEA sentence */
+#define CHECK_VALID 1 /* valid but without checksum */
+#define CHECK_CSVALID 2 /* valid with checksum OK */
+
/*
* Unit control structure
*/
@@ -169,11 +223,20 @@ struct nmeaunit {
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];
+ int32 last_daytime; /* last time-of-day stamp */
+ /* per sentence checksum seen flag */
+ u_char cksum_type[NMEA_ARRAY_SIZE];
+};
+
+/*
+ * helper for faster field access
+ */
+struct nmeadata {
+ char *base; /* buffer base */
+ char *cptr; /* current field ptr */
+ int blen; /* buffer length */
+ int cidx; /* current field index */
};
/*
@@ -194,13 +257,21 @@ static void nmea_timer (int, struct peer *);
#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*);
+
+/* parsing helpers */
+static int field_init (struct nmeadata *data, char *cp, int len);
+static char* field_parse(struct nmeadata *data, int fn);
+static void field_wipe (struct nmeadata *data, ...);
+static int parse_qual (const char *cp, char tag, int inv);
+static int parse_time (const char *cp, struct calendar *jd, long *nsec);
+static int parse_date (const char *cp, struct calendar *jd, enum date_fmt fmt);
+/* calendar / date helpers */
+static int unfold_day (struct calendar*, u_int32 rec_ui);
/*
+ * -------------------------------------------------------------------
* Transfer vector
+ * -------------------------------------------------------------------
*/
struct refclock refclock_nmea = {
nmea_start, /* start up driver */
@@ -213,7 +284,9 @@ struct refclock refclock_nmea = {
};
/*
+ * -------------------------------------------------------------------
* nmea_start - open the GPS devices and initialize data for processing
+ * -------------------------------------------------------------------
*/
static int
nmea_start(
@@ -230,15 +303,10 @@ nmea_start(
pp = peer->procptr;
- /*
- * Open serial port. Use CLK line discipline, if available.
+ /* Open serial port. Use CLK line discipline, if available. Use
+ * baudrate based on the value of bit 4/5/6
*/
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:
@@ -282,9 +350,8 @@ nmea_start(
* 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:
+ * 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
*/
@@ -305,7 +372,7 @@ nmea_start(
if ((nmea_tail = strtok(NULL,":")) == NULL)
return(0);
- nmea_port = atoi(nmea_tail);
+ nmea_port = strtoul(nmea_tail, NULL, 0);
if ((he = gethostbyname(nmea_host)) == NULL)
return(0);
@@ -331,9 +398,7 @@ nmea_start(
msyslog(LOG_NOTICE, "%s serial %s open at %s bps",
refnumtoa(&peer->srcadr), device, baudtext);
- /*
- * Allocate and initialize unit structure
- */
+ /* Allocate and initialize unit structure */
up = emalloc(sizeof(*up));
memset(up, 0, sizeof(*up));
pp->io.clock_recv = nmea_receive;
@@ -347,25 +412,31 @@ nmea_start(
return (0);
}
pp->unitptr = (caddr_t)up;
+ up->last_daytime = -1; /* force change detection on first valid message */
+ up->cksum_type[NMEA_GPRMC] = CHECK_CSVALID; /* force checksum on GPRMC, see below */
- /*
- * Initialize miscellaneous variables
- */
+ /* Initialize miscellaneous variables */
peer->precision = PRECISION;
pp->clockdesc = DESCRIPTION;
memcpy(&pp->refid, REFID, 4);
- gps_send(fd,"$PMOTG,RMC,0000*1D\r\n", peer);
-
+ /* Seems at least one MOTOROLA unit needs to switch on
+ * periodic transmission of $PGRMC. Though this is a
+ * misconfigured device, IMHO...
+ */
+ gps_send(fd, "PMOTG,RMC,0001", 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(
@@ -395,7 +466,9 @@ nmea_shutdown(
}
/*
+ * -------------------------------------------------------------------
* nmea_control - configure fudge params
+ * -------------------------------------------------------------------
*/
#ifdef HAVE_PPSAPI
static void
@@ -434,15 +507,12 @@ nmea_control(
return;
}
+ /* Light up the PPSAPI interface if not yet attempted. */
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
+ /* 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.
*/
@@ -468,11 +538,13 @@ nmea_control(
#endif /* HAVE_PPSAPI */
+#ifdef 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,
@@ -489,7 +561,7 @@ nmea_timer(
if (up->ppsapi_lit && up->ppsapi_gate &&
refclock_pps(peer, &up->atom, pp->sloppyclockflag) > 0) {
- up->pcount++,
+ up->pcount++;
peer->flags |= FLAG_PPS;
peer->precision = PPS_PRECISION;
}
@@ -498,39 +570,43 @@ nmea_timer(
#ifdef HAVE_PPSAPI
/*
+ * -------------------------------------------------------------------
+ * refclock_ppsrelate(...) -- correlate with PPS edge
+ *
* 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.
+ * 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.
+ * 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.
+ * 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.
+ * 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!)
+ * 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_offset()' must be used!)
+ * -------------------------------------------------------------------
*/
#define PPS_RELATE_NONE 0 /* no pps correlation possible */
#define PPS_RELATE_EDGE 1 /* recv time fixed, no phase lock */
@@ -606,120 +682,103 @@ refclock_ppsrelate(
#endif /* HAVE_PPSAPI */
/*
+ * -------------------------------------------------------------------
* nmea_receive - receive data from the serial interface
+ *
+ * This is the workhorse for NMEA data evaluation:
+ *
+ * + it checks all NMEA data, and rejects sentences that are not valid
+ * NMEA sentences
+ * + it cecks whether a sentence is known and its processing is enabled
+ * + it parses the time and date data from the NMEA data string and
+ * augments the missing bits. (century in dat, whole date, ...)
+ * + it rejects data that is not from the first accepted sentence in a
+ * burst
+ * + it evetually replaces the PPS edge time for the receive time
+ * + it feeds the data to the internal processing stages.
+ * -------------------------------------------------------------------
*/
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 */
+ /* declare & init control structure ptrs */
+ struct peer *const peer = rbufp->recv_peer;
+ struct refclockproc *const pp = peer->procptr;
+ struct nmeaunit *const up = (struct nmeaunit*)pp->unitptr;
+
+ /* Use these variables to hold data until we decide its worth keeping */
+ struct nmeadata rdata;
char rd_lastcode[BMAX];
- l_fp rd_timestamp, reftime;
+ l_fp rd_timestamp, rd_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 gives back a "blank" line.
- * We can't have a well-formed sentence with less than 8 chars.
+ /* working stuff */
+ struct calendar date; /* to keep & convert the time stamp */
+ int sentence=0, rc_date=0, rc_time=0; /* results of name/date/time parsing */
+ int checkres;
+ int32 daytime;
+ char *cp;
+
+ /* Read the timecode and timestamp, then initialise field
+ * processing. The at the NMEA line end is translated
+ * to by the terminal input routines on most systems,
+ * and this gives us one spurious empty read per record which we
+ * better ignore silently.
*/
- if (0 == rd_lencode)
- return;
-
- if (rd_lencode < 8) {
+ rd_lencode = refclock_gtlin(rbufp, rd_lastcode,
+ sizeof(rd_lastcode), &rd_timestamp);
+ checkres = field_init(&rdata, rd_lastcode, rd_lencode);
+ switch (checkres) {
+ case CHECK_INVALID:
+ DPRINTF(1, ("nmea: invalid data: '%s'\n", rd_lastcode));
refclock_report(peer, CEVNT_BADREPLY);
+ case CHECK_EMPTY:
return;
+ default:
+ DPRINTF(1, ("nmea: gpsread: %d '%s'\n",
+ rd_lencode, rd_lastcode));
+ break;
}
-
- 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
- *
- * 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
- *
- * 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
+ /* --> below this point we have a valid NMEA sentence <-- */
+
+ /* Check sentence name. Skip first 2 chars (talker ID), to allow
+ * for $GLGGA and $GPGGA etc. Since the name field has at least 5
+ * chars we can simply shift the field start.
*/
-
- 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;
-
+ cp = field_parse(&rdata, 0) + 2;
+ if ( strncmp(cp, "RMC,", 4) == 0)
+ sentence = NMEA_GPRMC;
+ else if (strncmp(cp, "GGA,", 4) == 0)
+ sentence = NMEA_GPGGA;
+ else if (strncmp(cp, "GLL,", 4) == 0)
+ sentence = NMEA_GPGLL;
+ else if (strncmp(cp, "ZDA,", 4) == 0)
+ sentence = NMEA_GPZDA;
+ else if (strncmp(cp, "ZDG,", 4) == 0)
+ sentence = NMEA_GPZDG;
+ else
+ return; /* not something we know about */
+
+ /* eventually output delay measurement now. */
+ if (peer->ttl & NMEA_DELAYMEAS_MASK) {
+ LIB_GETBUF(cp);
+ snprintf(cp, LIB_BUFLENGTH, "delay %0.6f %.*s",
+ ldexp(rd_timestamp.l_uf, -32),
+ (int)(strchr(rd_lastcode, ',') - rd_lastcode),
+ rd_lastcode);
+ record_clock_stats(&peer->srcadr, cp);
+ }
+
/* 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)
+ if ( (peer->ttl & NMEA_MESSAGE_MASK ) &&
+ !(peer->ttl & sentence_mode[sentence]) )
return;
- /*
+ /* make sure it came in clean
+ *
* 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
@@ -728,285 +787,153 @@ nmea_receive(
* 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.
+ * we keep track of whether we've ever seen a valid checksum on
+ * a given sentence, and if so, reject future instances without
+ * checksum. ('up->cksum_type[NMEA_GPRMC]' is set in
+ * 'nmea_start()' to enforce checksums for $GPRMC right from the
+ * start.)
*/
- if (nmea_checksum_ok(rd_lastcode)) {
- up->cksum_seen[sentence] = TRUE;
- } else if (NMEA_GPRMC == sentence || up->cksum_seen[sentence]) {
+ if (up->cksum_type[sentence] <= (u_char)checkres)
+ up->cksum_type[sentence] = (u_char)checkres;
+ else {
+ DPRINTF(1, ("nmea: checksum missing: '%s'\n", rd_lastcode));
refclock_report(peer, CEVNT_BADREPLY);
return;
}
- cp = rd_lastcode;
+ /* $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;
- /* Grab field depending on clock string type */
- memset(&date, 0, sizeof(date));
- switch (sentence) {
+ DPRINTF(1, ("nmea: processing %d bytes, timecode '%s'\n",
+ rd_lencode, rd_lastcode));
+ /* Grab fields depending on clock string type and possibly wipe
+ * sensitive data from the last timecode.
+ */
+ memset(&date, 0, sizeof(date)); /* pristine state of stamp */
+ 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);
+ /* Check quality byte, fetch data & time; need recv
+ * date here to augment century to date */
+ ntpcal_ntp_to_date(&date, rd_timestamp.l_ui, NULL);
+ rc_time = parse_time(field_parse(&rdata, 1),
+ &date, &pp->nsec);
+ pp->leap = parse_qual(field_parse(&rdata, 2),
+ 'A', 0);
+ rc_date = parse_date(field_parse(&rdata, 9),
+ &date, DATE_1_DDMMYY);
+ if (CLK_FLAG4 & pp->sloppyclockflag)
+ field_wipe(&rdata, 3, 4, 5, 6, -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);
+ /* Check quality byte, fetch time only */
+ rc_time = parse_time(field_parse(&rdata, 1),
+ &date, &pp->nsec);
+ pp->leap = parse_qual(field_parse(&rdata, 6),
+ '0', 1);
+ rc_date = unfold_day(&date, rd_timestamp.l_ui);
+ if (CLK_FLAG4 & pp->sloppyclockflag)
+ field_wipe(&rdata, 2, 4, -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);
+ /* Check quality byte, fetch time only */
+ rc_time = parse_time(field_parse(&rdata, 5),
+ &date, &pp->nsec);
+ pp->leap = parse_qual(field_parse(&rdata, 6),
+ 'A', 0);
+ rc_date = unfold_day(&date, rd_timestamp.l_ui);
+ if (CLK_FLAG4 & pp->sloppyclockflag)
+ field_wipe(&rdata, 1, 3, -1);
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);
+ /* No quality. Assume best, fetch time & full date */
+ pp->leap = LEAP_NOWARNING;
+ rc_time = parse_time(field_parse(&rdata, 1),
+ &date, &pp->nsec);
+ rc_date = parse_date(field_parse(&rdata, 2),
+ &date, DATE_3_DDMMYYYY);
break;
+ case NMEA_GPZDG:
+ /* Check quality byte, fetch time & full date */
+ rc_time = parse_time(field_parse(&rdata, 1),
+ &date, &pp->nsec);
+ rc_date = parse_date(field_parse(&rdata, 2),
+ &date, DATE_3_DDMMYYYY);
+ pp->leap = parse_qual(field_parse(&rdata, 4),
+ '0', 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]));
+ /* Check sanity of time-of-day. */
+ if (rc_time <= 0) { /* no time or conversion error? */
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);
+ /* Check sanity of date. */
+ if (rc_date <= 0) { /* no date or conversion error? */
+ refclock_report(peer, CEVNT_BADDATE);
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)
+ /* Discard sentence if time-of-day (in seconds) did not change */
+ daytime = ntpcal_date_to_daysec(&date);
+ if (up->last_daytime == daytime)
return;
+ up->last_daytime = daytime;
+
+ DPRINTF(1, ("nmea_receive: effective timecode: %04u-%02u-%02u %02d:%02d:%02d\n",
+ date.year, date.month, date.monthday,
+ date.hour, date.minute, date.second));
+ /* Store data for statistical purposes... */
+ if (rd_lencode >= sizeof(pp->a_lastcode))
+ rd_lencode = sizeof(pp->a_lastcode) - 1;
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
- *
- * 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] = '_';
- }
+ memcpy(pp->a_lastcode, rd_lastcode, rd_lencode);
+ pp->a_lastcode[rd_lencode] = '\0';
+ pp->lastrec = rd_timestamp;
- }
-
- /*
- * 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.
+ /* 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.
- */
+ DTOLFP(pp->nsec * 1.0e-9, &rd_reftime);
+ rd_reftime.l_ui += caltontp(&date);
+
+ /* $GPZDG postprocessing first...
+ * $GPZDG indicates the second after the *next* PPS pulse. So
+ * we remove 1 second from the reference time now. And since
+ * GPS timescale will be used from now, tell and log this fact.
+ */
+ if (sentence == NMEA_GPZDG) {
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--;
+ rd_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.
+ up->tcount++; /* received true timestamp */
+ /* 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,
+ switch (refclock_ppsrelate(pp, &up->atom, &rd_reftime,
&rd_timestamp, pp->fudgetime1,
&rd_fudge))
{
@@ -1026,15 +953,20 @@ nmea_receive(
return;
#endif /* HAVE_PPSAPI */
- refclock_process_offset(pp, reftime, rd_timestamp, rd_fudge);
+ refclock_process_offset(pp, rd_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.
+ * Does the necessary bookkeeping stuff to keep the reported state of
+ * the clock in sync with reality.
+ *
+ * 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(
@@ -1048,53 +980,68 @@ nmea_poll(
pp = peer->procptr;
up = (struct nmeaunit *)pp->unitptr;
+# if 0
+ /*
+ * usually nmea_receive can get a timestamp every second, but
+ * at least one Motorola unit needs prompting each time. And
+ * since we may bail out early, we do this immediately.
+ */
+ gps_send(pp->io.fd,"PMOTG,RMC,0000", peer);
+#endif
+
/*
* Process median filter samples. If none received, declare a
* timeout and keep going.
*/
#ifdef HAVE_PPSAPI
- if (up->pcount == 0) {
+ /*
+ * If we don't have PPS pulses and time stamps, turn PPS down
+ * for now.
+ */
+ if (up->pcount == 0 || up->tcount == 0) {
peer->flags &= ~FLAG_PPS;
peer->precision = PRECISION;
+ up->ppsapi_gate = 0;
}
- if (up->tcount == 0) {
+ /*
+ * If we don't have real time stamps flush the median filter;
+ * it might contain stale PPS time stamps otherwise.
+ */
+ if (up->tcount == 0)
pp->coderecv = pp->codeproc;
- refclock_report(peer, CEVNT_TIMEOUT);
- return;
- }
+ /*
+ * reset counters for next cycle.
+ */
up->pcount = up->tcount = 0;
-#else /* HAVE_PPSAPI */
+#endif /* HAVE_PPSAPI */
+ /*
+ * If the median filter is empty, claim a timeout and leave
+ */
if (pp->coderecv == pp->codeproc) {
refclock_report(peer, CEVNT_TIMEOUT);
return;
}
-#endif /* HAVE_PPSAPI */
+ /* keep stats going */
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);
+ * as gps_send(fd, "rqts,u", peer);
+ *
+ * The function will create the necessary frame (start char, chksum,
+ * final CRLF) on the fly.
*
* 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(
@@ -1103,31 +1050,38 @@ gps_send(
struct peer *peer
)
{
- if (write(fd, cmd, strlen(cmd)) == -1) {
+ char buf[NMEA_PROTO_MAXLEN + 7]; /* $...*xy add 7 */
+ u_char len = NMEA_PROTO_MAXLEN;
+ u_char dcs = 0;
+ char *dst = buf;
+ char *end = buf + sizeof(buf) - 1; /* last char */
+
+ /*
+ * copy data to buffer, creating checksum and frame on the fly
+ */
+ if (*cmd == '$')
+ cmd++;
+ *dst++ = '$';
+ while (len-- && *cmd && *cmd != '*')
+ dcs ^= *dst++ = *cmd++;
+ snprintf(dst, end - dst, "*%02X\r\n", dcs);
+ *end = '\0';
+ dst += strlen(dst);
+ len = dst - buf;
+ DPRINTF(1, ("nmea: gps_send: '%.*s'\n", len-2, buf));
+
+ /* send out the whole stuff */
+ if (write(fd, buf, len) == -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
+ * -------------------------------------------------------------------
+ * helpers for faster field splitting
+ * -------------------------------------------------------------------
+ *
+ * set up a field record, check syntax and verify checksum
*
* format is $XXXXX,1,2,3,4*ML
*
@@ -1137,71 +1091,316 @@ field_parse(
*
* $GPGLL,5057.970,N,00146.110,E,142451,A*27
* $GPVTG,089.0,T,,,15.2,N,,*7F
+ *
+ * Some other constraints:
+ * + The field name must at least 5 upcase characters or digits and must
+ * start with a character.
+ * + The checksum (if present) must be uppercase hex digits.
+ * + The length of a sentence is limited to 80 characters (not including
+ * the final CR/LF nor the checksum, but including the leading '$')
+ *
+ * Return values:
+ * + CHECK_INVALID
+ * The data does not form a valid NMEA sentence or a checksum error
+ * occurred.
+ * + CHECK_VALID
+ * The data is a valid NMEA sentence but contains no checksum.
+ * + CHECK_CSVALID
+ * The data is a valid NMEA sentence and passed the checksum test.
+ * -------------------------------------------------------------------
*/
-int
-nmea_checksum_ok(
- const char *sentence
- )
+static int
+field_init(
+ struct nmeadata *data, /* context structure */
+ char *cptr, /* start of raw data */
+ int dlen) /* data len, not counting trailing NUL */
{
- u_char my_cs;
- u_long input_cs;
- const char *p;
+ u_char cs_l=0, cs_r=0; /* checksum local computed / remote given */
+ char *eptr, tmp; /* end ptr and char buffer */
+
+ /* some basic input constraints */
+ if (dlen < 0)
+ dlen = 0;
+ eptr = cptr + dlen;
+ *eptr = '\0';
+
+ /* load data context */
+ data->base = cptr;
+ data->cptr = cptr;
+ data->cidx = 0;
+ data->blen = dlen;
+
+ /* syntax check follows here. check allowed character
+ * sequences, updating the local computed checksum as we go.
+ */
- my_cs = 0;
- p = sentence;
+ /* -*- start character: [$] */
+ if (*cptr == '\0')
+ return CHECK_EMPTY;
+ if (*cptr++ != '$')
+ return CHECK_INVALID;
- if ('$' != *p++)
- return 0;
+ /* -*- advance context beyond start character */
+ data->base++;
+ data->cptr++;
+ data->blen--;
+
+ /* -*- field name: [A-Z][A-Z0-9]{4+}[,] */
+ if (*cptr < 'A' || *cptr > 'Z')
+ return CHECK_INVALID;
+ cs_l ^= *cptr++;
+ while ((*cptr >= 'A' && *cptr <= 'Z') ||
+ (*cptr >= '0' && *cptr <= '9') )
+ cs_l ^= *cptr++;
+ if (*cptr != ',' || (cptr - data->base) < NMEA_PROTO_IDLEN)
+ return CHECK_INVALID;
+ cs_l ^= *cptr++;
+
+ /* -*- data: [^*]* */
+ while (*cptr && *cptr != '*')
+ cs_l ^= *cptr++;
+ if (*cptr == '\0')
+ return CHECK_VALID;
+ if (*cptr != '*' || cptr != eptr - 3 ||
+ (cptr - data->base) >= NMEA_PROTO_MAXLEN)
+ return CHECK_INVALID;
+
+ /* -*- checksum field: [*][0-9A-F]{2}$ */
+ for (cptr++; (tmp = *cptr) != '\0'; cptr++)
+ if (tmp >= '0' && tmp <= '9')
+ cs_r = (cs_r << 4) + (tmp - '0');
+ else if (tmp >= 'A' && tmp <= 'F')
+ cs_r = (cs_r << 4) + (tmp - 'A' + 10);
+ else
+ break;
- for ( ; *p && '*' != *p; p++) {
+ /* -*- make sure we are at end of string and csum matches */
+ if (cptr != eptr || cs_l != cs_r)
+ return CHECK_INVALID;
- my_cs ^= *p;
- }
+ return CHECK_CSVALID;
+}
- if ('*' != *p++)
- return 0;
+/*
+ * -------------------------------------------------------------------
+ * fetch a data field by index, zero being the name field. If this
+ * function is called repeatedly with increasing indices, the total load
+ * is O(n), n being the length of the string; if it is called with
+ * decreasing indices, the total load is O(n^2). Try not to go backwards
+ * too often.
+ * -------------------------------------------------------------------
+ */
+static char *
+field_parse(
+ struct nmeadata *data,
+ int fn )
+{
+ char tmp;
- if (0 == p[0] || 0 == p[1] || 0 != p[2])
- return 0;
+ if (fn < data->cidx) {
+ data->cidx = 0;
+ data->cptr = data->base;
+ }
+ while ((fn > data->cidx) && (tmp = *data->cptr) != '\0') {
+ data->cidx += (tmp == ',');
+ data->cptr++;
+ }
+ return data->cptr;
+}
- if (0 == hextoint(p, &input_cs))
- return 0;
+/*
+ * -------------------------------------------------------------------
+ * Wipe (that is, overwrite with '_') data fields and the checksum in
+ * the last timecode. The list of field indices is given as integers
+ * in a varargs list, preferrably in ascending order and terminated by
+ * a negative field index.
+ *
+ * A maximum number of 8 fields can be overwritten at once to guard
+ * against runaway (that is, unterminated) argument lists.
+ *
+ * This function affects what a remote user can see with
+ *
+ * ntpq -c clockvar
+ *
+ * Note that this also removes the wiped fields from any clockstats
+ * log. 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.
+ * -------------------------------------------------------------------
+ */
+static void
+field_wipe(
+ struct nmeadata *data,
+ ... ) /* this must be a list of ints, terminated by a value less
+ * than zero */
+{
+ va_list va; /* vararg index list */
+ int fcnt = 8; /* safeguard against runaway arglist */
+ int fidx; /* field to nuke, or -1 for checksum */
+ char *cp = NULL; /* overwrite destination */
+
+ va_start(va, data);
+ do {
+ fidx = va_arg(va, int);
+ if (fidx >= 0 && fidx <= NMEA_PROTO_FIELDS) {
+ cp = field_parse(data, fidx);
+ } else {
+ cp = data->base + data->blen;
+ if (data->blen >= 3 && cp[-3] == '*')
+ cp -= 2;
+ }
+ for (/*NOP*/; '\0' != *cp && '*' != *cp && ',' != *cp; cp++)
+ if ('.' != *cp)
+ *cp = '_';
+ } while (fcnt-- && fidx >= 0);
+ va_end(va);
+}
- if (my_cs != input_cs)
- return 0;
+/*
+ * -------------------------------------------------------------------
+ * PARSING HELPERS
+ * -------------------------------------------------------------------
+ *
+ * Check sync status
+ *
+ * If the character at the data field start matches the tag value,
+ * return LEAP_NOWARNING and LEAP_NOTINSYNC otherwise. If the 'inverted'
+ * flag is given, just the opposite value is returned. If there is no
+ * data field (*cp points to the NUL byte) the reult is LEAP_NOTINSYNC.
+ * -------------------------------------------------------------------
+ */
+static int
+parse_qual(
+ const char *dp ,
+ char tag,
+ int inv)
+{
+ static const int table[2] = { LEAP_NOTINSYNC, LEAP_NOWARNING };
- return 1;
+ return table[ *dp && ((*dp == tag) == !inv) ];
}
/*
* -------------------------------------------------------------------
- * funny calendar-oriented stuff -- a bit hard to grok.
+ * Parse a time stamp in HHMMSS[.sss] format with error checking.
+ *
+ * returns 1 on success, -1 on failure
* -------------------------------------------------------------------
*/
+static int
+parse_time(
+ const char *dp,
+ struct calendar *jd, /* result pointer */
+ long *ns) /* optional storage for nsec fraction */
+{
+ static const unsigned long weight[4] = {
+ 0, 100000000, 10000000, 1000000
+ };
+
+ int rc;
+ unsigned int h, m, s, p1, p2;
+ unsigned long f;
+
+ rc = sscanf(dp, "%2u%2u%2u%n.%3lu%n", &h, &m, &s, &p1, &f, &p2);
+ if (rc < 3 || p1 != 6) {
+ DPRINTF(1, ("nmea: invalid time code: '%.6s'\n", dp));
+ return -1;
+ }
+
+ /* value sanity check */
+ if (h > 23 || m > 59 || s > 60) {
+ DPRINTF(1, ("nmea: invalid time spec %02u:%02u:%02u\n",
+ h, m, s));
+ return -1;
+ }
+
+ jd->hour = h;
+ jd->minute = m;
+ jd->second = s;
+ /* if we have and need a fraction, scale it up to nanoseconds. */
+ if (ns) {
+ if (rc == 4)
+ *ns = f * weight[p2 - p1 - 1];
+ else
+ *ns = 0;
+ }
+
+ return 1;
+}
+
/*
- * 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.
+ * -------------------------------------------------------------------
+ * Parse a date string from an NMEA sentence. This could either be a
+ * partial date in DDMMYY format in one field, or DD,MM,YYYY full date
+ * spec spanning three fields. This function does some extensive error
+ * checking to make sure the date string was consistent.
+ *
+ * A 2-digit year is expanded into full year spec around the year found
+ * in 'jd->year'. This should be in -79/+19 years around the true time,
+ * or the result will be off by 100 years. 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.
+ *
+ * returns 1 on success, -1 on failure
+ * -------------------------------------------------------------------
*/
-static time_t
-nmea_periodic_unfold(
- time_t pivot,
- time_t value,
- time_t period)
+static int
+parse_date(
+ const char *dp,
+ struct calendar *jd , /* result pointer, may contain a year */
+ enum date_fmt fmt )
{
- /*
- * 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;
+ int rc, ybase;
+ unsigned int y, m, d, p;
+
+ switch (fmt) {
+ case DATE_1_DDMMYY:
+ rc = sscanf(dp, "%2u%2u%2u%n", &d, &m, &y, &p);
+ if (rc != 3 || p != 6) {
+ DPRINTF(1, ("nmea: invalid date code: '%.6s'\n",
+ dp));
+ return -1;
+ }
+ /* augment century, based on year in 'jd-year' */
+ ybase = (int)(jd->year ? jd->year : 1990) - 20;
+ y = ntpcal_periodic_extend(ybase, y, 100);
+ break;
+
+ case DATE_3_DDMMYYYY:
+ rc = sscanf(dp, "%2u,%2u,%4u%n", &d, &m, &y, &p);
+ if (rc != 3 || p != 10) {
+ DPRINTF(1, ("nmea: invalid date code: '%.10s'\n",
+ dp));
+ return -1;
+ }
+ break;
+
+ default:
+ DPRINTF(1, ("nmea: invalid parse format: %d\n", fmt));
+ return -1;
+ }
+
+ /* value sanity check */
+ if (d < 1 || d > 31 || m < 1 || m > 12) {
+ DPRINTF(1, ("nmea: invalid date spec (YMD) %04u:%02u:%02u\n",
+ y, m, d));
+ return -1;
+ }
+
+ /* store results */
+ jd->monthday = d;
+ jd->month = m;
+ jd->year = y;
+
+ return 1;
}
/*
+ * -------------------------------------------------------------------
+ * funny calendar-oriented stuff -- perhaps a bit hard to grok.
+ * -------------------------------------------------------------------
+ *
* 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.
@@ -1212,65 +1411,41 @@ nmea_periodic_unfold(
* 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
+ * The function updates the calendar structure it also uses as
* input to fetch the time from.
+ * -------------------------------------------------------------------
*/
-static void
-nmea_day_unfold(
- struct calendar *jd)
+static int
+unfold_day(
+ struct calendar *jd ,
+ u_int32 rec_ui)
{
- 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;
- }
-}
+ vint64 rec_qw;
+ ntpcal_split rec_ds;
+ int cvtres;
-/*
- * 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);
+ /*
+ * basically this is the peridiodic extension of the receive
+ * time - 12hrs to the time-of-day with a period of 1 day.
+ * But we would have to execute this in 64bit arithmetic, and we
+ * cannot assume we can do this; therefore this is done
+ * manually.
+ *
+ * Caveat: The time spec in '*jd' must be normalised; the time
+ * parsing function takes care of this.
+ */
+ rec_qw = ntpcal_ntp_to_ntp(rec_ui, NULL);
+ rec_ds = ntpcal_daysplit(&rec_qw);
+
+ rec_ds.lo = ntpcal_date_to_daysec(jd) - rec_ds.lo;
+ if (rec_ds.lo < -SECSPERDAY/2)
+ rec_ds.hi++;
+ if (rec_ds.lo >= SECSPERDAY/2)
+ rec_ds.hi--;
+
+ cvtres = ntpcal_rd_to_date(jd, rec_ds.hi + DAY_NTP_STARTS);
+ return (cvtres >= 0) ? 1 : -1;
}
-
#else
int refclock_nmea_bs;
#endif /* REFCLOCK && CLOCK_NMEA */