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