xref: /freebsd/contrib/ntp/ntpd/refclock_hpgps.c (revision 1e413cf93298b5b97441a21d9a50fdcd0ee9945e)
1 /*
2  * refclock_hpgps - clock driver for HP 58503A GPS receiver
3  */
4 
5 #ifdef HAVE_CONFIG_H
6 # include <config.h>
7 #endif
8 
9 #if defined(REFCLOCK) && defined(CLOCK_HPGPS)
10 
11 #include "ntpd.h"
12 #include "ntp_io.h"
13 #include "ntp_refclock.h"
14 #include "ntp_stdlib.h"
15 
16 #include <stdio.h>
17 #include <ctype.h>
18 
19 /* Version 0.1 April  1, 1995
20  *         0.2 April 25, 1995
21  *             tolerant of missing timecode response prompt and sends
22  *             clear status if prompt indicates error;
23  *             can use either local time or UTC from receiver;
24  *             can get receiver status screen via flag4
25  *
26  * WARNING!: This driver is UNDER CONSTRUCTION
27  * Everything in here should be treated with suspicion.
28  * If it looks wrong, it probably is.
29  *
30  * Comments and/or questions to: Dave Vitanye
31  *                               Hewlett Packard Company
32  *                               dave@scd.hp.com
33  *                               (408) 553-2856
34  *
35  * Thanks to the author of the PST driver, which was the starting point for
36  * this one.
37  *
38  * This driver supports the HP 58503A Time and Frequency Reference Receiver.
39  * This receiver uses HP SmartClock (TM) to implement an Enhanced GPS receiver.
40  * The receiver accuracy when locked to GPS in normal operation is better
41  * than 1 usec. The accuracy when operating in holdover is typically better
42  * than 10 usec. per day.
43  *
44  * The receiver should be operated with factory default settings.
45  * Initial driver operation: expects the receiver to be already locked
46  * to GPS, configured and able to output timecode format 2 messages.
47  *
48  * The driver uses the poll sequence :PTIME:TCODE? to get a response from
49  * the receiver. The receiver responds with a timecode string of ASCII
50  * printing characters, followed by a <cr><lf>, followed by a prompt string
51  * issued by the receiver, in the following format:
52  * T#yyyymmddhhmmssMFLRVcc<cr><lf>scpi >
53  *
54  * The driver processes the response at the <cr> and <lf>, so what the
55  * driver sees is the prompt from the previous poll, followed by this
56  * timecode. The prompt from the current poll is (usually) left unread until
57  * the next poll. So (except on the very first poll) the driver sees this:
58  *
59  * scpi > T#yyyymmddhhmmssMFLRVcc<cr><lf>
60  *
61  * The T is the on-time character, at 980 msec. before the next 1PPS edge.
62  * The # is the timecode format type. We look for format 2.
63  * Without any of the CLK or PPS stuff, then, the receiver buffer timestamp
64  * at the <cr> is 24 characters later, which is about 25 msec. at 9600 bps,
65  * so the first approximation for fudge time1 is nominally -0.955 seconds.
66  * This number probably needs adjusting for each machine / OS type, so far:
67  *  -0.955000 on an HP 9000 Model 712/80 HP-UX 9.05
68  *  -0.953175 on an HP 9000 Model 370    HP-UX 9.10
69  *
70  * This receiver also provides a 1PPS signal, but I haven't figured out
71  * how to deal with any of the CLK or PPS stuff yet. Stay tuned.
72  *
73  */
74 
75 /*
76  * Fudge Factors
77  *
78  * Fudge time1 is used to accomodate the timecode serial interface adjustment.
79  * Fudge flag4 can be set to request a receiver status screen summary, which
80  * is recorded in the clockstats file.
81  */
82 
83 /*
84  * Interface definitions
85  */
86 #define	DEVICE		"/dev/hpgps%d" /* device name and unit */
87 #define	SPEED232	B9600	/* uart speed (9600 baud) */
88 #define	PRECISION	(-10)	/* precision assumed (about 1 ms) */
89 #define	REFID		"GPS\0"	/*  reference ID */
90 #define	DESCRIPTION	"HP 58503A GPS Time and Frequency Reference Receiver"
91 
92 #define SMAX            23*80+1 /* for :SYSTEM:PRINT? status screen response */
93 
94 #define MTZONE          2       /* number of fields in timezone reply */
95 #define MTCODET2        12      /* number of fields in timecode format T2 */
96 #define NTCODET2        21      /* number of chars to checksum in format T2 */
97 
98 /*
99  * Tables to compute the day of year from yyyymmdd timecode.
100  * Viva la leap.
101  */
102 static int day1tab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
103 static int day2tab[] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
104 
105 /*
106  * Unit control structure
107  */
108 struct hpgpsunit {
109 	int	pollcnt;	/* poll message counter */
110 	int     tzhour;         /* timezone offset, hours */
111 	int     tzminute;       /* timezone offset, minutes */
112 	int     linecnt;        /* set for expected multiple line responses */
113 	char	*lastptr;	/* pointer to receiver response data */
114 	char    statscrn[SMAX]; /* receiver status screen buffer */
115 };
116 
117 /*
118  * Function prototypes
119  */
120 static	int	hpgps_start	P((int, struct peer *));
121 static	void	hpgps_shutdown	P((int, struct peer *));
122 static	void	hpgps_receive	P((struct recvbuf *));
123 static	void	hpgps_poll	P((int, struct peer *));
124 
125 /*
126  * Transfer vector
127  */
128 struct	refclock refclock_hpgps = {
129 	hpgps_start,		/* start up driver */
130 	hpgps_shutdown,		/* shut down driver */
131 	hpgps_poll,		/* transmit poll message */
132 	noentry,		/* not used (old hpgps_control) */
133 	noentry,		/* initialize driver */
134 	noentry,		/* not used (old hpgps_buginfo) */
135 	NOFLAGS			/* not used */
136 };
137 
138 
139 /*
140  * hpgps_start - open the devices and initialize data for processing
141  */
142 static int
143 hpgps_start(
144 	int unit,
145 	struct peer *peer
146 	)
147 {
148 	register struct hpgpsunit *up;
149 	struct refclockproc *pp;
150 	int fd;
151 	char device[20];
152 
153 	/*
154 	 * Open serial port. Use CLK line discipline, if available.
155 	 */
156 	(void)sprintf(device, DEVICE, unit);
157 	if (!(fd = refclock_open(device, SPEED232, LDISC_CLK)))
158 		return (0);
159 
160 	/*
161 	 * Allocate and initialize unit structure
162 	 */
163 	if (!(up = (struct hpgpsunit *)
164 	      emalloc(sizeof(struct hpgpsunit)))) {
165 		(void) close(fd);
166 		return (0);
167 	}
168 	memset((char *)up, 0, sizeof(struct hpgpsunit));
169 	pp = peer->procptr;
170 	pp->io.clock_recv = hpgps_receive;
171 	pp->io.srcclock = (caddr_t)peer;
172 	pp->io.datalen = 0;
173 	pp->io.fd = fd;
174 	if (!io_addclock(&pp->io)) {
175 		(void) close(fd);
176 		free(up);
177 		return (0);
178 	}
179 	pp->unitptr = (caddr_t)up;
180 
181 	/*
182 	 * Initialize miscellaneous variables
183 	 */
184 	peer->precision = PRECISION;
185 	pp->clockdesc = DESCRIPTION;
186 	memcpy((char *)&pp->refid, REFID, 4);
187 	up->tzhour = 0;
188 	up->tzminute = 0;
189 
190 	*up->statscrn = '\0';
191 	up->lastptr = up->statscrn;
192 	up->pollcnt = 2;
193 
194 	/*
195 	 * Get the identifier string, which is logged but otherwise ignored,
196 	 * and get the local timezone information
197 	 */
198 	up->linecnt = 1;
199 	if (write(pp->io.fd, "*IDN?\r:PTIME:TZONE?\r", 20) != 20)
200 	    refclock_report(peer, CEVNT_FAULT);
201 
202 	return (1);
203 }
204 
205 
206 /*
207  * hpgps_shutdown - shut down the clock
208  */
209 static void
210 hpgps_shutdown(
211 	int unit,
212 	struct peer *peer
213 	)
214 {
215 	register struct hpgpsunit *up;
216 	struct refclockproc *pp;
217 
218 	pp = peer->procptr;
219 	up = (struct hpgpsunit *)pp->unitptr;
220 	io_closeclock(&pp->io);
221 	free(up);
222 }
223 
224 
225 /*
226  * hpgps_receive - receive data from the serial interface
227  */
228 static void
229 hpgps_receive(
230 	struct recvbuf *rbufp
231 	)
232 {
233 	register struct hpgpsunit *up;
234 	struct refclockproc *pp;
235 	struct peer *peer;
236 	l_fp trtmp;
237 	char tcodechar1;        /* identifies timecode format */
238 	char tcodechar2;        /* identifies timecode format */
239 	char timequal;          /* time figure of merit: 0-9 */
240 	char freqqual;          /* frequency figure of merit: 0-3 */
241 	char leapchar;          /* leapsecond: + or 0 or - */
242 	char servchar;          /* request for service: 0 = no, 1 = yes */
243 	char syncchar;          /* time info is invalid: 0 = no, 1 = yes */
244 	short expectedsm;       /* expected timecode byte checksum */
245 	short tcodechksm;       /* computed timecode byte checksum */
246 	int i,m,n;
247 	int month, day, lastday;
248 	char *tcp;              /* timecode pointer (skips over the prompt) */
249 	char prompt[BMAX];      /* prompt in response from receiver */
250 
251 	/*
252 	 * Initialize pointers and read the receiver response
253 	 */
254 	peer = (struct peer *)rbufp->recv_srcclock;
255 	pp = peer->procptr;
256 	up = (struct hpgpsunit *)pp->unitptr;
257 	*pp->a_lastcode = '\0';
258 	pp->lencode = refclock_gtlin(rbufp, pp->a_lastcode, BMAX, &trtmp);
259 
260 #ifdef DEBUG
261 	if (debug)
262 	    printf("hpgps: lencode: %d timecode:%s\n",
263 		   pp->lencode, pp->a_lastcode);
264 #endif
265 
266 	/*
267 	 * If there's no characters in the reply, we can quit now
268 	 */
269 	if (pp->lencode == 0)
270 	    return;
271 
272 	/*
273 	 * If linecnt is greater than zero, we are getting information only,
274 	 * such as the receiver identification string or the receiver status
275 	 * screen, so put the receiver response at the end of the status
276 	 * screen buffer. When we have the last line, write the buffer to
277 	 * the clockstats file and return without further processing.
278 	 *
279 	 * If linecnt is zero, we are expecting either the timezone
280 	 * or a timecode. At this point, also write the response
281 	 * to the clockstats file, and go on to process the prompt (if any),
282 	 * timezone, or timecode and timestamp.
283 	 */
284 
285 
286 	if (up->linecnt-- > 0) {
287 		if ((int)(pp->lencode + 2) <= (SMAX - (up->lastptr - up->statscrn))) {
288 			*up->lastptr++ = '\n';
289 			(void)strcpy(up->lastptr, pp->a_lastcode);
290 			up->lastptr += pp->lencode;
291 		}
292 		if (up->linecnt == 0)
293 		    record_clock_stats(&peer->srcadr, up->statscrn);
294 
295 		return;
296 	}
297 
298 	record_clock_stats(&peer->srcadr, pp->a_lastcode);
299 	pp->lastrec = trtmp;
300 
301 	up->lastptr = up->statscrn;
302 	*up->lastptr = '\0';
303 	up->pollcnt = 2;
304 
305 	/*
306 	 * We get down to business: get a prompt if one is there, issue
307 	 * a clear status command if it contains an error indication.
308 	 * Next, check for either the timezone reply or the timecode reply
309 	 * and decode it.  If we don't recognize the reply, or don't get the
310 	 * proper number of decoded fields, or get an out of range timezone,
311 	 * or if the timecode checksum is bad, then we declare bad format
312 	 * and exit.
313 	 *
314 	 * Timezone format (including nominal prompt):
315 	 * scpi > -H,-M<cr><lf>
316 	 *
317 	 * Timecode format (including nominal prompt):
318 	 * scpi > T2yyyymmddhhmmssMFLRVcc<cr><lf>
319 	 *
320 	 */
321 
322 	(void)strcpy(prompt,pp->a_lastcode);
323 	tcp = strrchr(pp->a_lastcode,'>');
324 	if (tcp == NULL)
325 	    tcp = pp->a_lastcode;
326 	else
327 	    tcp++;
328 	prompt[tcp - pp->a_lastcode] = '\0';
329 	while ((*tcp == ' ') || (*tcp == '\t')) tcp++;
330 
331 	/*
332 	 * deal with an error indication in the prompt here
333 	 */
334 	if (strrchr(prompt,'E') > strrchr(prompt,'s')){
335 #ifdef DEBUG
336 		if (debug)
337 		    printf("hpgps: error indicated in prompt: %s\n", prompt);
338 #endif
339 		if (write(pp->io.fd, "*CLS\r\r", 6) != 6)
340 		    refclock_report(peer, CEVNT_FAULT);
341 	}
342 
343 	/*
344 	 * make sure we got a timezone or timecode format and
345 	 * then process accordingly
346 	 */
347 	m = sscanf(tcp,"%c%c", &tcodechar1, &tcodechar2);
348 
349 	if (m != 2){
350 #ifdef DEBUG
351 		if (debug)
352 		    printf("hpgps: no format indicator\n");
353 #endif
354 		refclock_report(peer, CEVNT_BADREPLY);
355 		return;
356 	}
357 
358 	switch (tcodechar1) {
359 
360 	    case '+':
361 	    case '-':
362 		m = sscanf(tcp,"%d,%d", &up->tzhour, &up->tzminute);
363 		if (m != MTZONE) {
364 #ifdef DEBUG
365 			if (debug)
366 			    printf("hpgps: only %d fields recognized in timezone\n", m);
367 #endif
368 			refclock_report(peer, CEVNT_BADREPLY);
369 			return;
370 		}
371 		if ((up->tzhour < -12) || (up->tzhour > 13) ||
372 		    (up->tzminute < -59) || (up->tzminute > 59)){
373 #ifdef DEBUG
374 			if (debug)
375 			    printf("hpgps: timezone %d, %d out of range\n",
376 				   up->tzhour, up->tzminute);
377 #endif
378 			refclock_report(peer, CEVNT_BADREPLY);
379 			return;
380 		}
381 		return;
382 
383 	    case 'T':
384 		break;
385 
386 	    default:
387 #ifdef DEBUG
388 		if (debug)
389 		    printf("hpgps: unrecognized reply format %c%c\n",
390 			   tcodechar1, tcodechar2);
391 #endif
392 		refclock_report(peer, CEVNT_BADREPLY);
393 		return;
394 	} /* end of tcodechar1 switch */
395 
396 
397 	switch (tcodechar2) {
398 
399 	    case '2':
400 		m = sscanf(tcp,"%*c%*c%4d%2d%2d%2d%2d%2d%c%c%c%c%c%2hx",
401 			   &pp->year, &month, &day, &pp->hour, &pp->minute, &pp->second,
402 			   &timequal, &freqqual, &leapchar, &servchar, &syncchar,
403 			   &expectedsm);
404 		n = NTCODET2;
405 
406 		if (m != MTCODET2){
407 #ifdef DEBUG
408 			if (debug)
409 			    printf("hpgps: only %d fields recognized in timecode\n", m);
410 #endif
411 			refclock_report(peer, CEVNT_BADREPLY);
412 			return;
413 		}
414 		break;
415 
416 	    default:
417 #ifdef DEBUG
418 		if (debug)
419 		    printf("hpgps: unrecognized timecode format %c%c\n",
420 			   tcodechar1, tcodechar2);
421 #endif
422 		refclock_report(peer, CEVNT_BADREPLY);
423 		return;
424 	} /* end of tcodechar2 format switch */
425 
426 	/*
427 	 * Compute and verify the checksum.
428 	 * Characters are summed starting at tcodechar1, ending at just
429 	 * before the expected checksum.  Bail out if incorrect.
430 	 */
431 	tcodechksm = 0;
432 	while (n-- > 0) tcodechksm += *tcp++;
433 	tcodechksm &= 0x00ff;
434 
435 	if (tcodechksm != expectedsm) {
436 #ifdef DEBUG
437 		if (debug)
438 		    printf("hpgps: checksum %2hX doesn't match %2hX expected\n",
439 			   tcodechksm, expectedsm);
440 #endif
441 		refclock_report(peer, CEVNT_BADREPLY);
442 		return;
443 	}
444 
445 	/*
446 	 * Compute the day of year from the yyyymmdd format.
447 	 */
448 	if (month < 1 || month > 12 || day < 1) {
449 		refclock_report(peer, CEVNT_BADTIME);
450 		return;
451 	}
452 
453 	if ( ! isleap_4(pp->year) ) {				/* Y2KFixes */
454 		/* not a leap year */
455 		if (day > day1tab[month - 1]) {
456 			refclock_report(peer, CEVNT_BADTIME);
457 			return;
458 		}
459 		for (i = 0; i < month - 1; i++) day += day1tab[i];
460 		lastday = 365;
461 	} else {
462 		/* a leap year */
463 		if (day > day2tab[month - 1]) {
464 			refclock_report(peer, CEVNT_BADTIME);
465 			return;
466 		}
467 		for (i = 0; i < month - 1; i++) day += day2tab[i];
468 		lastday = 366;
469 	}
470 
471 	/*
472 	 * Deal with the timezone offset here. The receiver timecode is in
473 	 * local time = UTC + :PTIME:TZONE, so SUBTRACT the timezone values.
474 	 * For example, Pacific Standard Time is -8 hours , 0 minutes.
475 	 * Deal with the underflows and overflows.
476 	 */
477 	pp->minute -= up->tzminute;
478 	pp->hour -= up->tzhour;
479 
480 	if (pp->minute < 0) {
481 		pp->minute += 60;
482 		pp->hour--;
483 	}
484 	if (pp->minute > 59) {
485 		pp->minute -= 60;
486 		pp->hour++;
487 	}
488 	if (pp->hour < 0)  {
489 		pp->hour += 24;
490 		day--;
491 		if (day < 1) {
492 			pp->year--;
493 			if ( isleap_4(pp->year) )		/* Y2KFixes */
494 			    day = 366;
495 			else
496 			    day = 365;
497 		}
498 	}
499 
500 	if (pp->hour > 23) {
501 		pp->hour -= 24;
502 		day++;
503 		if (day > lastday) {
504 			pp->year++;
505 			day = 1;
506 		}
507 	}
508 
509 	pp->day = day;
510 
511 	/*
512 	 * Decode the MFLRV indicators.
513 	 * NEED TO FIGURE OUT how to deal with the request for service,
514 	 * time quality, and frequency quality indicators some day.
515 	 */
516 	if (syncchar != '0') {
517 		pp->leap = LEAP_NOTINSYNC;
518 	}
519 	else {
520 		switch (leapchar) {
521 
522 		    case '+':
523 			pp->leap = LEAP_ADDSECOND;
524 			break;
525 
526 		    case '0':
527 			pp->leap = LEAP_NOWARNING;
528 			break;
529 
530 		    case '-':
531 			pp->leap = LEAP_DELSECOND;
532 			break;
533 
534 		    default:
535 #ifdef DEBUG
536 			if (debug)
537 			    printf("hpgps: unrecognized leap indicator: %c\n",
538 				   leapchar);
539 #endif
540 			refclock_report(peer, CEVNT_BADTIME);
541 			return;
542 		} /* end of leapchar switch */
543 	}
544 
545 	/*
546 	 * Process the new sample in the median filter and determine the
547 	 * reference clock offset and dispersion. We use lastrec as both
548 	 * the reference time and receive time in order to avoid being
549 	 * cute, like setting the reference time later than the receive
550 	 * time, which may cause a paranoid protocol module to chuck out
551 	 * the data.
552 	 */
553 	if (!refclock_process(pp)) {
554 		refclock_report(peer, CEVNT_BADTIME);
555 		return;
556 	}
557 	pp->lastref = pp->lastrec;
558 	refclock_receive(peer);
559 
560 	/*
561 	 * If CLK_FLAG4 is set, ask for the status screen response.
562 	 */
563 	if (pp->sloppyclockflag & CLK_FLAG4){
564 		up->linecnt = 22;
565 		if (write(pp->io.fd, ":SYSTEM:PRINT?\r", 15) != 15)
566 		    refclock_report(peer, CEVNT_FAULT);
567 	}
568 }
569 
570 
571 /*
572  * hpgps_poll - called by the transmit procedure
573  */
574 static void
575 hpgps_poll(
576 	int unit,
577 	struct peer *peer
578 	)
579 {
580 	register struct hpgpsunit *up;
581 	struct refclockproc *pp;
582 
583 	/*
584 	 * Time to poll the clock. The HP 58503A responds to a
585 	 * ":PTIME:TCODE?" by returning a timecode in the format specified
586 	 * above. If nothing is heard from the clock for two polls,
587 	 * declare a timeout and keep going.
588 	 */
589 	pp = peer->procptr;
590 	up = (struct hpgpsunit *)pp->unitptr;
591 	if (up->pollcnt == 0)
592 	    refclock_report(peer, CEVNT_TIMEOUT);
593 	else
594 	    up->pollcnt--;
595 	if (write(pp->io.fd, ":PTIME:TCODE?\r", 14) != 14) {
596 		refclock_report(peer, CEVNT_FAULT);
597 	}
598 	else
599 	    pp->polls++;
600 }
601 
602 #else
603 int refclock_hpgps_bs;
604 #endif /* REFCLOCK */
605