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