1 /*        $NetBSD: refclock_hpgps.c,v 1.6 2024/08/18 20:47:18 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  */
112 static int day1tab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
113 static int day2tab[] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
114 
115 /*
116  * Unit control structure
117  */
118 struct 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  */
130 static    int       hpgps_start         (int, struct peer *);
131 static    void      hpgps_shutdown      (int, struct peer *);
132 static    void      hpgps_receive       (struct recvbuf *);
133 static    void      hpgps_poll          (int, struct peer *);
134 
135 /*
136  * Transfer vector
137  */
138 struct    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  */
152 static int
hpgps_start(int unit,struct peer * peer)153 hpgps_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(&peer->srcadr, 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 (refclock_write(peer, "*IDN?\r:PTIME:TZONE?\r", 20, NULL) != 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
hpgps_shutdown(int unit,struct peer * peer)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 = 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  */
245 static void
hpgps_receive(struct recvbuf * rbufp)246 hpgps_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 (refclock_write(peer, "*CLS\r\r", 6, NULL) != 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 (refclock_write(peer, ":SYSTEM:PRINT?\r", 15, NULL) != 15)
590                               refclock_report(peer, CEVNT_FAULT);
591           }
592 }
593 
594 
595 /*
596  * hpgps_poll - called by the transmit procedure
597  */
598 static void
hpgps_poll(int unit,struct peer * peer)599 hpgps_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 (refclock_write(peer, ":PTIME:TCODE?\r", 14, NULL) != 14) {
620                     refclock_report(peer, CEVNT_FAULT);
621           }
622           else
623               pp->polls++;
624 }
625 
626 #else
627 NONEMPTY_TRANSLATION_UNIT
628 #endif /* REFCLOCK */
629