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